From 70338b7eddfa8a012aa8cb26cbcc6352c6234e42 Mon Sep 17 00:00:00 2001 From: p1c2u Date: Fri, 23 Feb 2024 20:02:17 +0000 Subject: [PATCH] AWS API Gateway with Amazon Lambda integrations support --- docs/integrations/aws.rst | 70 ++++++ docs/integrations/index.rst | 1 + openapi_core/contrib/aws/__init__.py | 17 ++ openapi_core/contrib/aws/datatypes.py | 104 +++++++++ openapi_core/contrib/aws/finders.py | 11 + openapi_core/contrib/aws/requests.py | 174 +++++++++++++++ openapi_core/contrib/aws/responses.py | 81 +++++++ openapi_core/contrib/aws/types.py | 5 + openapi_core/contrib/aws/util.py | 9 + poetry.lock | 202 ++++++++---------- pyproject.toml | 5 + .../http-api/events/http-proxy-catch-all.json | 44 ++++ .../data/v3.0/http-api/events/http-proxy.json | 42 ++++ .../v3.0/http-api/events/lambda-proxy-v2.json | 46 ++++ .../v3.0/http-api/events/lambda-proxy.json | 97 +++++++++ .../v3.0/http-api/http_api_with_apig_ext.yaml | 104 +++++++++ .../contrib/aws/test_aws_validation.py | 81 +++++++ 17 files changed, 979 insertions(+), 114 deletions(-) create mode 100644 docs/integrations/aws.rst create mode 100644 openapi_core/contrib/aws/__init__.py create mode 100644 openapi_core/contrib/aws/datatypes.py create mode 100644 openapi_core/contrib/aws/finders.py create mode 100644 openapi_core/contrib/aws/requests.py create mode 100644 openapi_core/contrib/aws/responses.py create mode 100644 openapi_core/contrib/aws/types.py create mode 100644 openapi_core/contrib/aws/util.py create mode 100644 tests/integration/contrib/aws/data/v3.0/http-api/events/http-proxy-catch-all.json create mode 100644 tests/integration/contrib/aws/data/v3.0/http-api/events/http-proxy.json create mode 100644 tests/integration/contrib/aws/data/v3.0/http-api/events/lambda-proxy-v2.json create mode 100644 tests/integration/contrib/aws/data/v3.0/http-api/events/lambda-proxy.json create mode 100644 tests/integration/contrib/aws/data/v3.0/http-api/http_api_with_apig_ext.yaml create mode 100644 tests/integration/contrib/aws/test_aws_validation.py diff --git a/docs/integrations/aws.rst b/docs/integrations/aws.rst new file mode 100644 index 00000000..31e258a5 --- /dev/null +++ b/docs/integrations/aws.rst @@ -0,0 +1,70 @@ +Amazon API Gateway +================== + +This section describes integration with `Amazon API Gateway `__. + +It is useful for: + +* `AWS Proxy integrations (i.e. AWS Lambda) for HTTP API `__ where Lambda functions handle events from API Gateway (Amazon API Gateway event format version 1.0 and 2.0). +* `HTTP Proxy integrations for HTTP API ` where HTTP service handle events from API Gateway. +* `AWS Lambda function URLs `__ where Lambda functions handle events from dedicated HTTP(S) endpoint (Amazon API Gateway event format version 2.0). + +ANY method +---------- + +Amazon API Gateway defines special ``ANY`` method that catches all HTTP methods. It is specified as `x-amazon-apigateway-any-method `__ OpenAPI extension. The extension is handled within custom path finder and can be used by setting ``path_finder_cls`` to be ``APIGatewayPathFinder``: + +.. code-block:: python + :emphasize-lines: 1,4 + + from openapi_core.contrib.aws import APIGatewayPathFinder + + config = Config( + path_finder_cls=APIGatewayPathFinder, + ) + openapi = OpenAPI.from_file_path('openapi.json', config=config) + +Low level +--------- + +The integration defines classes useful for low level integration. + +AWS Proxy event +^^^^^^^^^^^^^^^ + +Use ``APIGatewayAWSProxyV2OpenAPIRequest`` to create OpenAPI request from an API Gateway event (format version 2.0): + +.. code-block:: python + + from openapi_core.contrib.aws import APIGatewayAWSProxyV2OpenAPIRequest + + def handler(event, context): + openapi_request = APIGatewayAWSProxyV2OpenAPIRequest(event) + result = openapi.unmarshal_request(openapi_request) + return { + "statusCode": 200, + "body": "Hello world", + } + +If you use format version 1.0, then import and use ``APIGatewayAWSProxyOpenAPIRequest``. + +Response +^^^^^^^^ + +Use ``APIGatewayEventV2ResponseOpenAPIResponse`` to create OpenAPI response from API Gateway event (format version 2.0) response: + +.. code-block:: python + + from openapi_core.contrib.aws import APIGatewayEventV2ResponseOpenAPIResponse + + def handler(event, context): + openapi_request = APIGatewayEventV2OpenAPIRequest(event) + response = { + "statusCode": 200, + "body": "Hello world", + } + openapi_response = APIGatewayEventV2ResponseOpenAPIResponse(response) + result = openapi.unmarshal_response(openapi_request, openapi_response) + return response + +If you use format version 1.0, then import and use ``APIGatewayEventResponseOpenAPIResponse``. diff --git a/docs/integrations/index.rst b/docs/integrations/index.rst index f48c8cc9..eccb2c62 100644 --- a/docs/integrations/index.rst +++ b/docs/integrations/index.rst @@ -6,6 +6,7 @@ Openapi-core integrates with your popular libraries and frameworks. Each integra .. toctree:: :maxdepth: 1 + aws aiohttp bottle django diff --git a/openapi_core/contrib/aws/__init__.py b/openapi_core/contrib/aws/__init__.py new file mode 100644 index 00000000..ec41fd06 --- /dev/null +++ b/openapi_core/contrib/aws/__init__.py @@ -0,0 +1,17 @@ +"""OpenAPI core contrib django module""" + +from openapi_core.contrib.aws.finders import APIGatewayPathFinder +from openapi_core.contrib.aws.requests import APIGatewayAWSProxyOpenAPIRequest +from openapi_core.contrib.aws.requests import APIGatewayAWSProxyV2OpenAPIRequest +from openapi_core.contrib.aws.requests import APIGatewayHTTPProxyOpenAPIRequest +from openapi_core.contrib.aws.responses import APIGatewayEventResponseOpenAPIResponse +from openapi_core.contrib.aws.responses import APIGatewayEventV2ResponseOpenAPIResponse + +__all__ = [ + "APIGatewayPathFinder", + "APIGatewayAWSProxyOpenAPIRequest", + "APIGatewayAWSProxyV2OpenAPIRequest", + "APIGatewayHTTPProxyOpenAPIRequest", + "APIGatewayEventResponseOpenAPIResponse", + "APIGatewayEventV2ResponseOpenAPIResponse", +] diff --git a/openapi_core/contrib/aws/datatypes.py b/openapi_core/contrib/aws/datatypes.py new file mode 100644 index 00000000..cc1473d4 --- /dev/null +++ b/openapi_core/contrib/aws/datatypes.py @@ -0,0 +1,104 @@ +from typing import Dict +from typing import List +from typing import Literal +from typing import Optional + +from pydantic import Field +from pydantic.dataclasses import dataclass + +API_GATEWAY_EVENT_CONFIG = dict(extra="allow") + + +@dataclass(frozen=True) +class APIGatewayEventRequestContext: + """AWS API Gateway event request context""" + model_config = API_GATEWAY_EVENT_CONFIG + + resourceId: str + + +@dataclass(frozen=True) +class APIGatewayEvent: + """AWS API Gateway event""" + model_config = API_GATEWAY_EVENT_CONFIG + + headers: Dict[str, str] + + path: str + httpMethod: str + resource: str + requestContext: APIGatewayEventRequestContext + + queryStringParameters: Optional[Dict[str, str]] = None + isBase64Encoded: Optional[bool] = None + body: Optional[str] = None + pathParameters: Optional[Dict[str, str]] = None + stageVariables: Optional[Dict[str, str]] = None + + multiValueHeaders: Optional[Dict[str, List[str]]] = None + version: Optional[str] = "1.0" + multiValueQueryStringParameters: Optional[Dict[str, List[str]]] = None + + +@dataclass(frozen=True) +class APIGatewayEventV2Http: + """AWS API Gateway event v2 HTTP""" + model_config = API_GATEWAY_EVENT_CONFIG + + method: str + path: str + + +@dataclass(frozen=True) +class APIGatewayEventV2RequestContext: + """AWS API Gateway event v2 request context""" + model_config = API_GATEWAY_EVENT_CONFIG + + http: APIGatewayEventV2Http + + +@dataclass(frozen=True) +class APIGatewayEventV2: + """AWS API Gateway event v2""" + model_config = API_GATEWAY_EVENT_CONFIG + + headers: Dict[str, str] + + version: Literal["2.0"] + routeKey: str + rawPath: str + rawQueryString: str + requestContext: APIGatewayEventV2RequestContext + + queryStringParameters: Optional[Dict[str, str]] = None + isBase64Encoded: Optional[bool] = None + body: Optional[str] = None + pathParameters: Optional[Dict[str, str]] = None + stageVariables: Optional[Dict[str, str]] = None + + cookies: Optional[List[str]] = None + + +@dataclass(frozen=True) +class APIGatewayEventResponse: + """AWS API Gateway event response""" + model_config = API_GATEWAY_EVENT_CONFIG + + body: str + isBase64Encoded: bool + statusCode: int + headers: Dict[str, str] + multiValueHeaders: Dict[str, List[str]] + + +@dataclass(frozen=True) +class APIGatewayEventV2Response: + """AWS API Gateway event v2 response""" + model_config = API_GATEWAY_EVENT_CONFIG + + body: str + isBase64Encoded: bool = False + statusCode: int = 200 + headers: Dict[str, str] = Field( + default_factory=lambda: {"content-type": "application/json"} + ) diff --git a/openapi_core/contrib/aws/finders.py b/openapi_core/contrib/aws/finders.py new file mode 100644 index 00000000..b49b2de1 --- /dev/null +++ b/openapi_core/contrib/aws/finders.py @@ -0,0 +1,11 @@ +from openapi_core.templating.paths.finders import APICallPathFinder +from openapi_core.templating.paths.iterators import ( + CatchAllMethodOperationsIterator, +) + + +class APIGatewayPathFinder(APICallPathFinder): + operations_iterator = CatchAllMethodOperationsIterator( + "any", + "x-amazon-apigateway-any-method", + ) diff --git a/openapi_core/contrib/aws/requests.py b/openapi_core/contrib/aws/requests.py new file mode 100644 index 00000000..7c5cb758 --- /dev/null +++ b/openapi_core/contrib/aws/requests.py @@ -0,0 +1,174 @@ +from typing import Dict +from typing import Optional +from typing import Tuple +from urllib.parse import urljoin + +from werkzeug.datastructures import Headers +from werkzeug.datastructures import ImmutableMultiDict + +from openapi_core.contrib.aws.datatypes import APIGatewayEvent +from openapi_core.contrib.aws.datatypes import APIGatewayEventV2 +from openapi_core.contrib.aws.types import APIGatewayEventPayload +from openapi_core.contrib.aws.util import parse_forwarded +from openapi_core.datatypes import RequestParameters + + +class APIGatewayAWSProxyOpenAPIRequest: + """ + API Gateway AWS proxy event payload OpenAPI request. + + Designed to be used with API Gateway REST API specification exports for + integrations that use event v1 payload. Uses API Gateway event v1 + requestContext's resourceId. Requires APIGatewayPathFinder to resolve ANY methods. + """ + + def __init__(self, payload: APIGatewayEventPayload): + self.event = APIGatewayEvent(**payload) + + self.parameters = RequestParameters( + path=self.path_params, + query=ImmutableMultiDict(self.query_params), + header=Headers(self.event.headers), + cookie=ImmutableMultiDict(), + ) + + @property + def resource_id(self) -> Tuple[str, str]: + return self.event.requestContext.resourceId.split(" ") + + @property + def path_params(self) -> Dict[str, str]: + params = self.event.pathParameters + if params is None: + return {} + return params + + @property + def query_params(self) -> Dict[str, str]: + params = self.event.queryStringParameters + if params is None: + return {} + return params + + @property + def proto(self) -> str: + return self.event.headers.get("X-Forwarded-Proto", "https") + + @property + def host(self) -> str: + return self.event.headers["Host"] + + @property + def host_url(self) -> str: + return "://".join([self.proto, self.host]) + + @property + def path(self) -> str: + return self.event.path + + @property + def method(self) -> str: + return self.resource_id[0].lower() + + @property + def body(self) -> Optional[str]: + return self.event.body + + @property + def content_type(self) -> str: + return self.event.headers.get("Content-Type", "") + + @property + def path_pattern(self): + return self.resource_id[1] + + +class APIGatewayAWSProxyV2OpenAPIRequest: + """ + API Gateway AWS Proxy event v2 payload OpenAPI request. + + Designed to be used with API Gateway HTTP API specification exports for + integrations that use event v2 payload. Uses API Gateway event v2 routeKey + and rawPath data. Requires APIGatewayPathFinder to resolve ANY methods. + + .. note:: + API Gateway HTTP APIs don't support request validation + """ + + def __init__(self, payload: APIGatewayEventPayload): + self.event = APIGatewayEventV2(**payload) + + self.parameters = RequestParameters( + path=self.path_params, + query=ImmutableMultiDict(self.query_params), + header=Headers(self.event.headers), + cookie=ImmutableMultiDict(), + ) + + @property + def path_params(self) -> Dict[str, str]: + if self.event.pathParameters is None: + return {} + return self.event.pathParameters + + @property + def query_params(self) -> Dict[str, str]: + if self.event.queryStringParameters is None: + return {} + return self.event.queryStringParameters + + @property + def proto(self) -> str: + return self.event.headers.get("x-forwarded-proto", "https") + + @property + def host(self) -> str: + # use Forwarded header if available + if "forwarded" in self.event.headers: + forwarded = parse_forwarded(self.event.headers["forwarded"]) + if "host" in forwarded: + return forwarded["host"] + return self.event.headers["host"] + + @property + def host_url(self) -> str: + return "://".join([self.proto, self.host]) + + @property + def path(self) -> str: + return self.event.rawPath + + @property + def method(self) -> str: + return self.event.routeKey.split(" ")[0].lower() + + @property + def body(self) -> Optional[str]: + return self.event.body + + @property + def content_type(self) -> str: + return self.event.headers.get("content-type", "") + + +class APIGatewayHTTPProxyOpenAPIRequest(APIGatewayAWSProxyV2OpenAPIRequest): + """ + API Gateway HTTP proxy integration event payload OpenAPI request. + + Uses http integration path and method data. + + NOTE: If you use HTTP integration not in root path then you need to provide the base path + otherwise it won't find the correct path because it is not send with event. + """ + + def __init__(self, payload: APIGatewayEventPayload, base_path: str = "/"): + super().__init__(payload) + self.base_path = base_path + + @property + def path(self) -> str: + return urljoin(self.base_path, self.event.requestContext.http.path.lstrip('/')) + + @property + def method(self) -> str: + return self.event.requestContext.http.method.lower() diff --git a/openapi_core/contrib/aws/responses.py b/openapi_core/contrib/aws/responses.py new file mode 100644 index 00000000..74ef1c36 --- /dev/null +++ b/openapi_core/contrib/aws/responses.py @@ -0,0 +1,81 @@ +from json import dumps +from typing import Union + +from werkzeug.datastructures import Headers + +from openapi_core.contrib.aws.datatypes import APIGatewayEventResponse +from openapi_core.contrib.aws.datatypes import APIGatewayEventV2Response +from openapi_core.contrib.aws.types import APIGatewayEventResponsePayload + + +class APIGatewayEventResponseOpenAPIResponse: + """ + Converts an API Gateway event response payload to an OpenAPI request + """ + + def __init__(self, payload: APIGatewayEventResponsePayload): + self.response = APIGatewayEventResponse(**payload) + + @property + def data(self) -> str: + return self.response.body + + @property + def status_code(self) -> int: + return self.response.statusCode + + @property + def headers(self) -> Headers: + return Headers(self.response.headers) + + @property + def mimetype(self) -> str: + content_type = self.response.headers.get("Content-Type", "") + assert isinstance(content_type, str) + return content_type + + +class APIGatewayEventV2ResponseOpenAPIResponse: + """ + Converts an API Gateway event v2 response payload to an OpenAPI request + """ + + def __init__(self, payload: Union[APIGatewayEventResponsePayload, str]): + if not isinstance(payload, dict): + payload = self._construct_payload(payload) + elif "statusCode" not in payload: + body = dumps(payload) + payload = self._construct_payload(body) + + self.response = APIGatewayEventV2Response(**payload) + + @staticmethod + def _construct_payload(body: str) -> APIGatewayEventResponsePayload: + return { + "isBase64Encoded": False, + "statusCode": 200, + "headers": { + "content-type": "application/json", + }, + "body": body, + } + + @property + def data(self) -> str: + return self.response.body + + @property + def status_code(self) -> int: + return self.response.statusCode + + @property + def headers(self) -> Headers: + return Headers(self.response.headers) + + @property + def mimetype(self) -> str: + content_type = self.response.headers.get( + "content-type", "application/json" + ) + assert isinstance(content_type, str) + return content_type diff --git a/openapi_core/contrib/aws/types.py b/openapi_core/contrib/aws/types.py new file mode 100644 index 00000000..aa5a6b2e --- /dev/null +++ b/openapi_core/contrib/aws/types.py @@ -0,0 +1,5 @@ +from typing import Any +from typing import Dict + +APIGatewayEventPayload = Dict[str, Any] +APIGatewayEventResponsePayload = Dict[str, Any] diff --git a/openapi_core/contrib/aws/util.py b/openapi_core/contrib/aws/util.py new file mode 100644 index 00000000..a8500484 --- /dev/null +++ b/openapi_core/contrib/aws/util.py @@ -0,0 +1,9 @@ +from typing import Dict + + +def parse_forwarded(header: str) -> Dict[str, str]: + parts = {} + for part in header.split(";"): + k, v = part.split("=") + parts[k] = v + return parts diff --git a/poetry.lock b/poetry.lock index 8ff5e819..ca06c79f 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1558,18 +1558,18 @@ virtualenv = ">=20.10.0" [[package]] name = "pydantic" -version = "2.4.2" +version = "2.6.2" description = "Data validation using Python type hints" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "pydantic-2.4.2-py3-none-any.whl", hash = "sha256:bc3ddf669d234f4220e6e1c4d96b061abe0998185a8d7855c0126782b7abc8c1"}, - {file = "pydantic-2.4.2.tar.gz", hash = "sha256:94f336138093a5d7f426aac732dcfe7ab4eb4da243c88f891d65deb4a2556ee7"}, + {file = "pydantic-2.6.2-py3-none-any.whl", hash = "sha256:37a5432e54b12fecaa1049c5195f3d860a10e01bdfd24f1840ef14bd0d3aeab3"}, + {file = "pydantic-2.6.2.tar.gz", hash = "sha256:a09be1c3d28f3abe37f8a78af58284b236a92ce520105ddc91a6d29ea1176ba7"}, ] [package.dependencies] annotated-types = ">=0.4.0" -pydantic-core = "2.10.1" +pydantic-core = "2.16.3" typing-extensions = ">=4.6.1" [package.extras] @@ -1577,117 +1577,90 @@ email = ["email-validator (>=2.0.0)"] [[package]] name = "pydantic-core" -version = "2.10.1" +version = "2.16.3" description = "" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "pydantic_core-2.10.1-cp310-cp310-macosx_10_7_x86_64.whl", hash = "sha256:d64728ee14e667ba27c66314b7d880b8eeb050e58ffc5fec3b7a109f8cddbd63"}, - {file = "pydantic_core-2.10.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:48525933fea744a3e7464c19bfede85df4aba79ce90c60b94d8b6e1eddd67096"}, - {file = "pydantic_core-2.10.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ef337945bbd76cce390d1b2496ccf9f90b1c1242a3a7bc242ca4a9fc5993427a"}, - {file = "pydantic_core-2.10.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a1392e0638af203cee360495fd2cfdd6054711f2db5175b6e9c3c461b76f5175"}, - {file = "pydantic_core-2.10.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0675ba5d22de54d07bccde38997e780044dcfa9a71aac9fd7d4d7a1d2e3e65f7"}, - {file = "pydantic_core-2.10.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:128552af70a64660f21cb0eb4876cbdadf1a1f9d5de820fed6421fa8de07c893"}, - {file = "pydantic_core-2.10.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f6e6aed5818c264412ac0598b581a002a9f050cb2637a84979859e70197aa9e"}, - {file = "pydantic_core-2.10.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ecaac27da855b8d73f92123e5f03612b04c5632fd0a476e469dfc47cd37d6b2e"}, - {file = "pydantic_core-2.10.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b3c01c2fb081fced3bbb3da78510693dc7121bb893a1f0f5f4b48013201f362e"}, - {file = "pydantic_core-2.10.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:92f675fefa977625105708492850bcbc1182bfc3e997f8eecb866d1927c98ae6"}, - {file = "pydantic_core-2.10.1-cp310-none-win32.whl", hash = "sha256:420a692b547736a8d8703c39ea935ab5d8f0d2573f8f123b0a294e49a73f214b"}, - {file = "pydantic_core-2.10.1-cp310-none-win_amd64.whl", hash = "sha256:0880e239827b4b5b3e2ce05e6b766a7414e5f5aedc4523be6b68cfbc7f61c5d0"}, - {file = "pydantic_core-2.10.1-cp311-cp311-macosx_10_7_x86_64.whl", hash = "sha256:073d4a470b195d2b2245d0343569aac7e979d3a0dcce6c7d2af6d8a920ad0bea"}, - {file = "pydantic_core-2.10.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:600d04a7b342363058b9190d4e929a8e2e715c5682a70cc37d5ded1e0dd370b4"}, - {file = "pydantic_core-2.10.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:39215d809470f4c8d1881758575b2abfb80174a9e8daf8f33b1d4379357e417c"}, - {file = "pydantic_core-2.10.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eeb3d3d6b399ffe55f9a04e09e635554012f1980696d6b0aca3e6cf42a17a03b"}, - {file = "pydantic_core-2.10.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a7a7902bf75779bc12ccfc508bfb7a4c47063f748ea3de87135d433a4cca7a2f"}, - {file = "pydantic_core-2.10.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3625578b6010c65964d177626fde80cf60d7f2e297d56b925cb5cdeda6e9925a"}, - {file = "pydantic_core-2.10.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:caa48fc31fc7243e50188197b5f0c4228956f97b954f76da157aae7f67269ae8"}, - {file = "pydantic_core-2.10.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:07ec6d7d929ae9c68f716195ce15e745b3e8fa122fc67698ac6498d802ed0fa4"}, - {file = "pydantic_core-2.10.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e6f31a17acede6a8cd1ae2d123ce04d8cca74056c9d456075f4f6f85de055607"}, - {file = "pydantic_core-2.10.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d8f1ebca515a03e5654f88411420fea6380fc841d1bea08effb28184e3d4899f"}, - {file = "pydantic_core-2.10.1-cp311-none-win32.whl", hash = "sha256:6db2eb9654a85ada248afa5a6db5ff1cf0f7b16043a6b070adc4a5be68c716d6"}, - {file = "pydantic_core-2.10.1-cp311-none-win_amd64.whl", hash = "sha256:4a5be350f922430997f240d25f8219f93b0c81e15f7b30b868b2fddfc2d05f27"}, - {file = "pydantic_core-2.10.1-cp311-none-win_arm64.whl", hash = "sha256:5fdb39f67c779b183b0c853cd6b45f7db84b84e0571b3ef1c89cdb1dfc367325"}, - {file = "pydantic_core-2.10.1-cp312-cp312-macosx_10_7_x86_64.whl", hash = "sha256:b1f22a9ab44de5f082216270552aa54259db20189e68fc12484873d926426921"}, - {file = "pydantic_core-2.10.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8572cadbf4cfa95fb4187775b5ade2eaa93511f07947b38f4cd67cf10783b118"}, - {file = "pydantic_core-2.10.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:db9a28c063c7c00844ae42a80203eb6d2d6bbb97070cfa00194dff40e6f545ab"}, - {file = "pydantic_core-2.10.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0e2a35baa428181cb2270a15864ec6286822d3576f2ed0f4cd7f0c1708472aff"}, - {file = "pydantic_core-2.10.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:05560ab976012bf40f25d5225a58bfa649bb897b87192a36c6fef1ab132540d7"}, - {file = "pydantic_core-2.10.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d6495008733c7521a89422d7a68efa0a0122c99a5861f06020ef5b1f51f9ba7c"}, - {file = "pydantic_core-2.10.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:14ac492c686defc8e6133e3a2d9eaf5261b3df26b8ae97450c1647286750b901"}, - {file = "pydantic_core-2.10.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8282bab177a9a3081fd3d0a0175a07a1e2bfb7fcbbd949519ea0980f8a07144d"}, - {file = "pydantic_core-2.10.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:aafdb89fdeb5fe165043896817eccd6434aee124d5ee9b354f92cd574ba5e78f"}, - {file = "pydantic_core-2.10.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:f6defd966ca3b187ec6c366604e9296f585021d922e666b99c47e78738b5666c"}, - {file = "pydantic_core-2.10.1-cp312-none-win32.whl", hash = "sha256:7c4d1894fe112b0864c1fa75dffa045720a194b227bed12f4be7f6045b25209f"}, - {file = "pydantic_core-2.10.1-cp312-none-win_amd64.whl", hash = "sha256:5994985da903d0b8a08e4935c46ed8daf5be1cf217489e673910951dc533d430"}, - {file = "pydantic_core-2.10.1-cp312-none-win_arm64.whl", hash = "sha256:0d8a8adef23d86d8eceed3e32e9cca8879c7481c183f84ed1a8edc7df073af94"}, - {file = "pydantic_core-2.10.1-cp37-cp37m-macosx_10_7_x86_64.whl", hash = "sha256:9badf8d45171d92387410b04639d73811b785b5161ecadabf056ea14d62d4ede"}, - {file = "pydantic_core-2.10.1-cp37-cp37m-macosx_11_0_arm64.whl", hash = "sha256:ebedb45b9feb7258fac0a268a3f6bec0a2ea4d9558f3d6f813f02ff3a6dc6698"}, - {file = "pydantic_core-2.10.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cfe1090245c078720d250d19cb05d67e21a9cd7c257698ef139bc41cf6c27b4f"}, - {file = "pydantic_core-2.10.1-cp37-cp37m-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e357571bb0efd65fd55f18db0a2fb0ed89d0bb1d41d906b138f088933ae618bb"}, - {file = "pydantic_core-2.10.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b3dcd587b69bbf54fc04ca157c2323b8911033e827fffaecf0cafa5a892a0904"}, - {file = "pydantic_core-2.10.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9c120c9ce3b163b985a3b966bb701114beb1da4b0468b9b236fc754783d85aa3"}, - {file = "pydantic_core-2.10.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15d6bca84ffc966cc9976b09a18cf9543ed4d4ecbd97e7086f9ce9327ea48891"}, - {file = "pydantic_core-2.10.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5cabb9710f09d5d2e9e2748c3e3e20d991a4c5f96ed8f1132518f54ab2967221"}, - {file = "pydantic_core-2.10.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:82f55187a5bebae7d81d35b1e9aaea5e169d44819789837cdd4720d768c55d15"}, - {file = "pydantic_core-2.10.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:1d40f55222b233e98e3921df7811c27567f0e1a4411b93d4c5c0f4ce131bc42f"}, - {file = "pydantic_core-2.10.1-cp37-none-win32.whl", hash = "sha256:14e09ff0b8fe6e46b93d36a878f6e4a3a98ba5303c76bb8e716f4878a3bee92c"}, - {file = "pydantic_core-2.10.1-cp37-none-win_amd64.whl", hash = "sha256:1396e81b83516b9d5c9e26a924fa69164156c148c717131f54f586485ac3c15e"}, - {file = "pydantic_core-2.10.1-cp38-cp38-macosx_10_7_x86_64.whl", hash = "sha256:6835451b57c1b467b95ffb03a38bb75b52fb4dc2762bb1d9dbed8de31ea7d0fc"}, - {file = "pydantic_core-2.10.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b00bc4619f60c853556b35f83731bd817f989cba3e97dc792bb8c97941b8053a"}, - {file = "pydantic_core-2.10.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0fa467fd300a6f046bdb248d40cd015b21b7576c168a6bb20aa22e595c8ffcdd"}, - {file = "pydantic_core-2.10.1-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d99277877daf2efe074eae6338453a4ed54a2d93fb4678ddfe1209a0c93a2468"}, - {file = "pydantic_core-2.10.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fa7db7558607afeccb33c0e4bf1c9a9a835e26599e76af6fe2fcea45904083a6"}, - {file = "pydantic_core-2.10.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:aad7bd686363d1ce4ee930ad39f14e1673248373f4a9d74d2b9554f06199fb58"}, - {file = "pydantic_core-2.10.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:443fed67d33aa85357464f297e3d26e570267d1af6fef1c21ca50921d2976302"}, - {file = "pydantic_core-2.10.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:042462d8d6ba707fd3ce9649e7bf268633a41018d6a998fb5fbacb7e928a183e"}, - {file = "pydantic_core-2.10.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:ecdbde46235f3d560b18be0cb706c8e8ad1b965e5c13bbba7450c86064e96561"}, - {file = "pydantic_core-2.10.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:ed550ed05540c03f0e69e6d74ad58d026de61b9eaebebbaaf8873e585cbb18de"}, - {file = "pydantic_core-2.10.1-cp38-none-win32.whl", hash = "sha256:8cdbbd92154db2fec4ec973d45c565e767ddc20aa6dbaf50142676484cbff8ee"}, - {file = "pydantic_core-2.10.1-cp38-none-win_amd64.whl", hash = "sha256:9f6f3e2598604956480f6c8aa24a3384dbf6509fe995d97f6ca6103bb8c2534e"}, - {file = "pydantic_core-2.10.1-cp39-cp39-macosx_10_7_x86_64.whl", hash = "sha256:655f8f4c8d6a5963c9a0687793da37b9b681d9ad06f29438a3b2326d4e6b7970"}, - {file = "pydantic_core-2.10.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e570ffeb2170e116a5b17e83f19911020ac79d19c96f320cbfa1fa96b470185b"}, - {file = "pydantic_core-2.10.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:64322bfa13e44c6c30c518729ef08fda6026b96d5c0be724b3c4ae4da939f875"}, - {file = "pydantic_core-2.10.1-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:485a91abe3a07c3a8d1e082ba29254eea3e2bb13cbbd4351ea4e5a21912cc9b0"}, - {file = "pydantic_core-2.10.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f7c2b8eb9fc872e68b46eeaf835e86bccc3a58ba57d0eedc109cbb14177be531"}, - {file = "pydantic_core-2.10.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a5cb87bdc2e5f620693148b5f8f842d293cae46c5f15a1b1bf7ceeed324a740c"}, - {file = "pydantic_core-2.10.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:25bd966103890ccfa028841a8f30cebcf5875eeac8c4bde4fe221364c92f0c9a"}, - {file = "pydantic_core-2.10.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f323306d0556351735b54acbf82904fe30a27b6a7147153cbe6e19aaaa2aa429"}, - {file = "pydantic_core-2.10.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0c27f38dc4fbf07b358b2bc90edf35e82d1703e22ff2efa4af4ad5de1b3833e7"}, - {file = "pydantic_core-2.10.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:f1365e032a477c1430cfe0cf2856679529a2331426f8081172c4a74186f1d595"}, - {file = "pydantic_core-2.10.1-cp39-none-win32.whl", hash = "sha256:a1c311fd06ab3b10805abb72109f01a134019739bd3286b8ae1bc2fc4e50c07a"}, - {file = "pydantic_core-2.10.1-cp39-none-win_amd64.whl", hash = "sha256:ae8a8843b11dc0b03b57b52793e391f0122e740de3df1474814c700d2622950a"}, - {file = "pydantic_core-2.10.1-pp310-pypy310_pp73-macosx_10_7_x86_64.whl", hash = "sha256:d43002441932f9a9ea5d6f9efaa2e21458221a3a4b417a14027a1d530201ef1b"}, - {file = "pydantic_core-2.10.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:fcb83175cc4936a5425dde3356f079ae03c0802bbdf8ff82c035f8a54b333521"}, - {file = "pydantic_core-2.10.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:962ed72424bf1f72334e2f1e61b68f16c0e596f024ca7ac5daf229f7c26e4208"}, - {file = "pydantic_core-2.10.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2cf5bb4dd67f20f3bbc1209ef572a259027c49e5ff694fa56bed62959b41e1f9"}, - {file = "pydantic_core-2.10.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e544246b859f17373bed915182ab841b80849ed9cf23f1f07b73b7c58baee5fb"}, - {file = "pydantic_core-2.10.1-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:c0877239307b7e69d025b73774e88e86ce82f6ba6adf98f41069d5b0b78bd1bf"}, - {file = "pydantic_core-2.10.1-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:53df009d1e1ba40f696f8995683e067e3967101d4bb4ea6f667931b7d4a01357"}, - {file = "pydantic_core-2.10.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:a1254357f7e4c82e77c348dabf2d55f1d14d19d91ff025004775e70a6ef40ada"}, - {file = "pydantic_core-2.10.1-pp37-pypy37_pp73-macosx_10_7_x86_64.whl", hash = "sha256:524ff0ca3baea164d6d93a32c58ac79eca9f6cf713586fdc0adb66a8cdeab96a"}, - {file = "pydantic_core-2.10.1-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3f0ac9fb8608dbc6eaf17956bf623c9119b4db7dbb511650910a82e261e6600f"}, - {file = "pydantic_core-2.10.1-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:320f14bd4542a04ab23747ff2c8a778bde727158b606e2661349557f0770711e"}, - {file = "pydantic_core-2.10.1-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:63974d168b6233b4ed6a0046296803cb13c56637a7b8106564ab575926572a55"}, - {file = "pydantic_core-2.10.1-pp37-pypy37_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:417243bf599ba1f1fef2bb8c543ceb918676954734e2dcb82bf162ae9d7bd514"}, - {file = "pydantic_core-2.10.1-pp37-pypy37_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:dda81e5ec82485155a19d9624cfcca9be88a405e2857354e5b089c2a982144b2"}, - {file = "pydantic_core-2.10.1-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:14cfbb00959259e15d684505263d5a21732b31248a5dd4941f73a3be233865b9"}, - {file = "pydantic_core-2.10.1-pp38-pypy38_pp73-macosx_10_7_x86_64.whl", hash = "sha256:631cb7415225954fdcc2a024119101946793e5923f6c4d73a5914d27eb3d3a05"}, - {file = "pydantic_core-2.10.1-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:bec7dd208a4182e99c5b6c501ce0b1f49de2802448d4056091f8e630b28e9a52"}, - {file = "pydantic_core-2.10.1-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:149b8a07712f45b332faee1a2258d8ef1fb4a36f88c0c17cb687f205c5dc6e7d"}, - {file = "pydantic_core-2.10.1-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4d966c47f9dd73c2d32a809d2be529112d509321c5310ebf54076812e6ecd884"}, - {file = "pydantic_core-2.10.1-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7eb037106f5c6b3b0b864ad226b0b7ab58157124161d48e4b30c4a43fef8bc4b"}, - {file = "pydantic_core-2.10.1-pp38-pypy38_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:154ea7c52e32dce13065dbb20a4a6f0cc012b4f667ac90d648d36b12007fa9f7"}, - {file = "pydantic_core-2.10.1-pp38-pypy38_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:e562617a45b5a9da5be4abe72b971d4f00bf8555eb29bb91ec2ef2be348cd132"}, - {file = "pydantic_core-2.10.1-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:f23b55eb5464468f9e0e9a9935ce3ed2a870608d5f534025cd5536bca25b1402"}, - {file = "pydantic_core-2.10.1-pp39-pypy39_pp73-macosx_10_7_x86_64.whl", hash = "sha256:e9121b4009339b0f751955baf4543a0bfd6bc3f8188f8056b1a25a2d45099934"}, - {file = "pydantic_core-2.10.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:0523aeb76e03f753b58be33b26540880bac5aa54422e4462404c432230543f33"}, - {file = "pydantic_core-2.10.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2e0e2959ef5d5b8dc9ef21e1a305a21a36e254e6a34432d00c72a92fdc5ecda5"}, - {file = "pydantic_core-2.10.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:da01bec0a26befab4898ed83b362993c844b9a607a86add78604186297eb047e"}, - {file = "pydantic_core-2.10.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f2e9072d71c1f6cfc79a36d4484c82823c560e6f5599c43c1ca6b5cdbd54f881"}, - {file = "pydantic_core-2.10.1-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:f36a3489d9e28fe4b67be9992a23029c3cec0babc3bd9afb39f49844a8c721c5"}, - {file = "pydantic_core-2.10.1-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f64f82cc3443149292b32387086d02a6c7fb39b8781563e0ca7b8d7d9cf72bd7"}, - {file = "pydantic_core-2.10.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:b4a6db486ac8e99ae696e09efc8b2b9fea67b63c8f88ba7a1a16c24a057a0776"}, - {file = "pydantic_core-2.10.1.tar.gz", hash = "sha256:0f8682dbdd2f67f8e1edddcbffcc29f60a6182b4901c367fc8c1c40d30bb0a82"}, + {file = "pydantic_core-2.16.3-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:75b81e678d1c1ede0785c7f46690621e4c6e63ccd9192af1f0bd9d504bbb6bf4"}, + {file = "pydantic_core-2.16.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9c865a7ee6f93783bd5d781af5a4c43dadc37053a5b42f7d18dc019f8c9d2bd1"}, + {file = "pydantic_core-2.16.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:162e498303d2b1c036b957a1278fa0899d02b2842f1ff901b6395104c5554a45"}, + {file = "pydantic_core-2.16.3-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2f583bd01bbfbff4eaee0868e6fc607efdfcc2b03c1c766b06a707abbc856187"}, + {file = "pydantic_core-2.16.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b926dd38db1519ed3043a4de50214e0d600d404099c3392f098a7f9d75029ff8"}, + {file = "pydantic_core-2.16.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:716b542728d4c742353448765aa7cdaa519a7b82f9564130e2b3f6766018c9ec"}, + {file = "pydantic_core-2.16.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc4ad7f7ee1a13d9cb49d8198cd7d7e3aa93e425f371a68235f784e99741561f"}, + {file = "pydantic_core-2.16.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bd87f48924f360e5d1c5f770d6155ce0e7d83f7b4e10c2f9ec001c73cf475c99"}, + {file = "pydantic_core-2.16.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:0df446663464884297c793874573549229f9eca73b59360878f382a0fc085979"}, + {file = "pydantic_core-2.16.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:4df8a199d9f6afc5ae9a65f8f95ee52cae389a8c6b20163762bde0426275b7db"}, + {file = "pydantic_core-2.16.3-cp310-none-win32.whl", hash = "sha256:456855f57b413f077dff513a5a28ed838dbbb15082ba00f80750377eed23d132"}, + {file = "pydantic_core-2.16.3-cp310-none-win_amd64.whl", hash = "sha256:732da3243e1b8d3eab8c6ae23ae6a58548849d2e4a4e03a1924c8ddf71a387cb"}, + {file = "pydantic_core-2.16.3-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:519ae0312616026bf4cedc0fe459e982734f3ca82ee8c7246c19b650b60a5ee4"}, + {file = "pydantic_core-2.16.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b3992a322a5617ded0a9f23fd06dbc1e4bd7cf39bc4ccf344b10f80af58beacd"}, + {file = "pydantic_core-2.16.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8d62da299c6ecb04df729e4b5c52dc0d53f4f8430b4492b93aa8de1f541c4aac"}, + {file = "pydantic_core-2.16.3-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2acca2be4bb2f2147ada8cac612f8a98fc09f41c89f87add7256ad27332c2fda"}, + {file = "pydantic_core-2.16.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1b662180108c55dfbf1280d865b2d116633d436cfc0bba82323554873967b340"}, + {file = "pydantic_core-2.16.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e7c6ed0dc9d8e65f24f5824291550139fe6f37fac03788d4580da0d33bc00c97"}, + {file = "pydantic_core-2.16.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a6b1bb0827f56654b4437955555dc3aeeebeddc47c2d7ed575477f082622c49e"}, + {file = "pydantic_core-2.16.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e56f8186d6210ac7ece503193ec84104da7ceb98f68ce18c07282fcc2452e76f"}, + {file = "pydantic_core-2.16.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:936e5db01dd49476fa8f4383c259b8b1303d5dd5fb34c97de194560698cc2c5e"}, + {file = "pydantic_core-2.16.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:33809aebac276089b78db106ee692bdc9044710e26f24a9a2eaa35a0f9fa70ba"}, + {file = "pydantic_core-2.16.3-cp311-none-win32.whl", hash = "sha256:ded1c35f15c9dea16ead9bffcde9bb5c7c031bff076355dc58dcb1cb436c4721"}, + {file = "pydantic_core-2.16.3-cp311-none-win_amd64.whl", hash = "sha256:d89ca19cdd0dd5f31606a9329e309d4fcbb3df860960acec32630297d61820df"}, + {file = "pydantic_core-2.16.3-cp311-none-win_arm64.whl", hash = "sha256:6162f8d2dc27ba21027f261e4fa26f8bcb3cf9784b7f9499466a311ac284b5b9"}, + {file = "pydantic_core-2.16.3-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:0f56ae86b60ea987ae8bcd6654a887238fd53d1384f9b222ac457070b7ac4cff"}, + {file = "pydantic_core-2.16.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c9bd22a2a639e26171068f8ebb5400ce2c1bc7d17959f60a3b753ae13c632975"}, + {file = "pydantic_core-2.16.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4204e773b4b408062960e65468d5346bdfe139247ee5f1ca2a378983e11388a2"}, + {file = "pydantic_core-2.16.3-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f651dd19363c632f4abe3480a7c87a9773be27cfe1341aef06e8759599454120"}, + {file = "pydantic_core-2.16.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aaf09e615a0bf98d406657e0008e4a8701b11481840be7d31755dc9f97c44053"}, + {file = "pydantic_core-2.16.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8e47755d8152c1ab5b55928ab422a76e2e7b22b5ed8e90a7d584268dd49e9c6b"}, + {file = "pydantic_core-2.16.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:500960cb3a0543a724a81ba859da816e8cf01b0e6aaeedf2c3775d12ee49cade"}, + {file = "pydantic_core-2.16.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:cf6204fe865da605285c34cf1172879d0314ff267b1c35ff59de7154f35fdc2e"}, + {file = "pydantic_core-2.16.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d33dd21f572545649f90c38c227cc8631268ba25c460b5569abebdd0ec5974ca"}, + {file = "pydantic_core-2.16.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:49d5d58abd4b83fb8ce763be7794d09b2f50f10aa65c0f0c1696c677edeb7cbf"}, + {file = "pydantic_core-2.16.3-cp312-none-win32.whl", hash = "sha256:f53aace168a2a10582e570b7736cc5bef12cae9cf21775e3eafac597e8551fbe"}, + {file = "pydantic_core-2.16.3-cp312-none-win_amd64.whl", hash = "sha256:0d32576b1de5a30d9a97f300cc6a3f4694c428d956adbc7e6e2f9cad279e45ed"}, + {file = "pydantic_core-2.16.3-cp312-none-win_arm64.whl", hash = "sha256:ec08be75bb268473677edb83ba71e7e74b43c008e4a7b1907c6d57e940bf34b6"}, + {file = "pydantic_core-2.16.3-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:b1f6f5938d63c6139860f044e2538baeee6f0b251a1816e7adb6cbce106a1f01"}, + {file = "pydantic_core-2.16.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:2a1ef6a36fdbf71538142ed604ad19b82f67b05749512e47f247a6ddd06afdc7"}, + {file = "pydantic_core-2.16.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:704d35ecc7e9c31d48926150afada60401c55efa3b46cd1ded5a01bdffaf1d48"}, + {file = "pydantic_core-2.16.3-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d937653a696465677ed583124b94a4b2d79f5e30b2c46115a68e482c6a591c8a"}, + {file = "pydantic_core-2.16.3-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c9803edf8e29bd825f43481f19c37f50d2b01899448273b3a7758441b512acf8"}, + {file = "pydantic_core-2.16.3-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:72282ad4892a9fb2da25defeac8c2e84352c108705c972db82ab121d15f14e6d"}, + {file = "pydantic_core-2.16.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f752826b5b8361193df55afcdf8ca6a57d0232653494ba473630a83ba50d8c9"}, + {file = "pydantic_core-2.16.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4384a8f68ddb31a0b0c3deae88765f5868a1b9148939c3f4121233314ad5532c"}, + {file = "pydantic_core-2.16.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:a4b2bf78342c40b3dc830880106f54328928ff03e357935ad26c7128bbd66ce8"}, + {file = "pydantic_core-2.16.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:13dcc4802961b5f843a9385fc821a0b0135e8c07fc3d9949fd49627c1a5e6ae5"}, + {file = "pydantic_core-2.16.3-cp38-none-win32.whl", hash = "sha256:e3e70c94a0c3841e6aa831edab1619ad5c511199be94d0c11ba75fe06efe107a"}, + {file = "pydantic_core-2.16.3-cp38-none-win_amd64.whl", hash = "sha256:ecdf6bf5f578615f2e985a5e1f6572e23aa632c4bd1dc67f8f406d445ac115ed"}, + {file = "pydantic_core-2.16.3-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:bda1ee3e08252b8d41fa5537413ffdddd58fa73107171a126d3b9ff001b9b820"}, + {file = "pydantic_core-2.16.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:21b888c973e4f26b7a96491c0965a8a312e13be108022ee510248fe379a5fa23"}, + {file = "pydantic_core-2.16.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:be0ec334369316fa73448cc8c982c01e5d2a81c95969d58b8f6e272884df0074"}, + {file = "pydantic_core-2.16.3-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b5b6079cc452a7c53dd378c6f881ac528246b3ac9aae0f8eef98498a75657805"}, + {file = "pydantic_core-2.16.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7ee8d5f878dccb6d499ba4d30d757111847b6849ae07acdd1205fffa1fc1253c"}, + {file = "pydantic_core-2.16.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7233d65d9d651242a68801159763d09e9ec96e8a158dbf118dc090cd77a104c9"}, + {file = "pydantic_core-2.16.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c6119dc90483a5cb50a1306adb8d52c66e447da88ea44f323e0ae1a5fcb14256"}, + {file = "pydantic_core-2.16.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:578114bc803a4c1ff9946d977c221e4376620a46cf78da267d946397dc9514a8"}, + {file = "pydantic_core-2.16.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d8f99b147ff3fcf6b3cc60cb0c39ea443884d5559a30b1481e92495f2310ff2b"}, + {file = "pydantic_core-2.16.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:4ac6b4ce1e7283d715c4b729d8f9dab9627586dafce81d9eaa009dd7f25dd972"}, + {file = "pydantic_core-2.16.3-cp39-none-win32.whl", hash = "sha256:e7774b570e61cb998490c5235740d475413a1f6de823169b4cf94e2fe9e9f6b2"}, + {file = "pydantic_core-2.16.3-cp39-none-win_amd64.whl", hash = "sha256:9091632a25b8b87b9a605ec0e61f241c456e9248bfdcf7abdf344fdb169c81cf"}, + {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:36fa178aacbc277bc6b62a2c3da95226520da4f4e9e206fdf076484363895d2c"}, + {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:dcca5d2bf65c6fb591fff92da03f94cd4f315972f97c21975398bd4bd046854a"}, + {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2a72fb9963cba4cd5793854fd12f4cfee731e86df140f59ff52a49b3552db241"}, + {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b60cc1a081f80a2105a59385b92d82278b15d80ebb3adb200542ae165cd7d183"}, + {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:cbcc558401de90a746d02ef330c528f2e668c83350f045833543cd57ecead1ad"}, + {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:fee427241c2d9fb7192b658190f9f5fd6dfe41e02f3c1489d2ec1e6a5ab1e04a"}, + {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f4cb85f693044e0f71f394ff76c98ddc1bc0953e48c061725e540396d5c8a2e1"}, + {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:b29eeb887aa931c2fcef5aa515d9d176d25006794610c264ddc114c053bf96fe"}, + {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:a425479ee40ff021f8216c9d07a6a3b54b31c8267c6e17aa88b70d7ebd0e5e5b"}, + {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:5c5cbc703168d1b7a838668998308018a2718c2130595e8e190220238addc96f"}, + {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:99b6add4c0b39a513d323d3b93bc173dac663c27b99860dd5bf491b240d26137"}, + {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75f76ee558751746d6a38f89d60b6228fa174e5172d143886af0f85aa306fd89"}, + {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:00ee1c97b5364b84cb0bd82e9bbf645d5e2871fb8c58059d158412fee2d33d8a"}, + {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:287073c66748f624be4cef893ef9174e3eb88fe0b8a78dc22e88eca4bc357ca6"}, + {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:ed25e1835c00a332cb10c683cd39da96a719ab1dfc08427d476bce41b92531fc"}, + {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:86b3d0033580bd6bbe07590152007275bd7af95f98eaa5bd36f3da219dcd93da"}, + {file = "pydantic_core-2.16.3.tar.gz", hash = "sha256:1cac689f80a3abab2d3c0048b29eea5751114054f032a941a32de4c852c59cad"}, ] [package.dependencies] @@ -2554,6 +2527,7 @@ testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "p [extras] aiohttp = ["aiohttp", "multidict"] +aws = ["pydantic"] django = ["django"] falcon = ["falcon"] fastapi = ["fastapi"] @@ -2564,4 +2538,4 @@ starlette = ["aioitertools", "starlette"] [metadata] lock-version = "2.0" python-versions = "^3.8.0" -content-hash = "71044a8cb0eff35c69c9b4e38849687d8b11814d2df86f5b547e786e7defa070" +content-hash = "c40b4f60414ac7b8151a3887af65878be458ce1e8f84e65ec6119383ef897b6b" diff --git a/pyproject.toml b/pyproject.toml index 3d55ac7e..8ba1a718 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,6 +12,9 @@ output = "reports/coverage.xml" [tool.mypy] files = "openapi_core" strict = true +plugins = [ + "pydantic.mypy" +] [[tool.mypy.overrides]] module = [ @@ -78,8 +81,10 @@ jsonschema = "^4.18.0" multidict = {version = "^6.0.4", optional = true} aioitertools = {version = "^0.11.0", optional = true} fastapi = {version = "^0.108.0", optional = true} +pydantic = {version = "^2.6.2", optional = true} [tool.poetry.extras] +aws = ["pydantic"] django = ["django"] falcon = ["falcon"] fastapi = ["fastapi"] diff --git a/tests/integration/contrib/aws/data/v3.0/http-api/events/http-proxy-catch-all.json b/tests/integration/contrib/aws/data/v3.0/http-api/events/http-proxy-catch-all.json new file mode 100644 index 00000000..30d16a4b --- /dev/null +++ b/tests/integration/contrib/aws/data/v3.0/http-api/events/http-proxy-catch-all.json @@ -0,0 +1,44 @@ +{ + "version": "2.0", + "routeKey": "$default", + "rawPath": "/12/", + "rawQueryString": "q=1", + "headers": { + "content-length": "0", + "x-amzn-tls-version": "TLSv1.3", + "x-forwarded-proto": "https", + "x-forwarded-port": "443", + "x-forwarded-for": "44.234.31.53", + "forwarded": "by=44.234.31.53;for=79.191.34.167;host=testapi.execute-api.us-west-2.amazonaws.com;proto=https", + "accept": "*/*", + "via": "HTTP/1.1 AmazonAPIGateway", + "x-amzn-tls-cipher-suite": "TLS_AES_128_GCM_SHA256", + "x-amzn-trace-id": "Self=1-661d921a-64f7833670ee8e067b95f249;Root=1-661d921a-47006f4f437ff0d62f67aff9", + "host": "testlambdafunction.lambda-url.us-west-2.on.aws", + "cache-control": "no-cache", + "accept-encoding": "gzip, deflate, br", + "user-agent": "python/7.37.3" + }, + "queryStringParameters": { + "q": "1" + }, + "requestContext": { + "accountId": "anonymous", + "apiId": "testlambdafunction", + "domainName": "testlambdafunction.lambda-url.us-west-2.on.aws", + "domainPrefix": "testlambdafunction", + "http": { + "method": "GET", + "path": "/12", + "protocol": "HTTP/1.1", + "sourceIp": "44.234.31.53", + "userAgent": "python/7.37.3" + }, + "requestId": "fb47f473-d1d0-4672-a325-7995de9e0596", + "routeKey": "$default", + "stage": "$default", + "time": "15/Apr/2024:20:46:18 +0000", + "timeEpoch": 1713213978505 + }, + "isBase64Encoded": false +} \ No newline at end of file diff --git a/tests/integration/contrib/aws/data/v3.0/http-api/events/http-proxy.json b/tests/integration/contrib/aws/data/v3.0/http-api/events/http-proxy.json new file mode 100644 index 00000000..d12e78d4 --- /dev/null +++ b/tests/integration/contrib/aws/data/v3.0/http-api/events/http-proxy.json @@ -0,0 +1,42 @@ +{ + "version": "2.0", + "routeKey": "$default", + "rawPath": "/", + "rawQueryString": "", + "headers": { + "content-length": "0", + "x-amzn-tls-version": "TLSv1.3", + "x-forwarded-proto": "https", + "testing": "testing", + "x-forwarded-port": "443", + "x-forwarded-for": "44.234.29.163", + "forwarded": "by=44.234.29.163;for=79.191.34.167;host=testapi.execute-api.us-west-2.amazonaws.com;proto=https", + "accept": "*/*", + "via": "HTTP/1.1 AmazonAPIGateway", + "x-amzn-tls-cipher-suite": "TLS_AES_128_GCM_SHA256", + "x-amzn-trace-id": "Self=1-661d9237-719f831c5d88398f0b9f5745;Root=1-661d9237-5da8931d48969520348c3b2a", + "host": "testlambdafunction.lambda-url.us-west-2.on.aws", + "cache-control": "no-cache", + "accept-encoding": "gzip, deflate, br", + "user-agent": "python/7.37.3" + }, + "requestContext": { + "accountId": "anonymous", + "apiId": "testlambdafunction", + "domainName": "testlambdafunction.lambda-url.us-west-2.on.aws", + "domainPrefix": "testlambdafunction", + "http": { + "method": "GET", + "path": "/", + "protocol": "HTTP/1.1", + "sourceIp": "44.234.29.163", + "userAgent": "python/7.37.3" + }, + "requestId": "17627a2a-c01c-45dc-98e6-46f559af3c42", + "routeKey": "$default", + "stage": "$default", + "time": "15/Apr/2024:20:46:47 +0000", + "timeEpoch": 1713214007846 + }, + "isBase64Encoded": false +} \ No newline at end of file diff --git a/tests/integration/contrib/aws/data/v3.0/http-api/events/lambda-proxy-v2.json b/tests/integration/contrib/aws/data/v3.0/http-api/events/lambda-proxy-v2.json new file mode 100644 index 00000000..2b436386 --- /dev/null +++ b/tests/integration/contrib/aws/data/v3.0/http-api/events/lambda-proxy-v2.json @@ -0,0 +1,46 @@ +{ + "version": "2.0", + "routeKey": "ANY /test-lambda-proxy-v2/{id}", + "rawPath": "/dev/test-lambda-proxy-v2/14", + "rawQueryString": "q=1", + "headers": { + "accept": "*/*", + "accept-encoding": "gzip, deflate, br", + "cache-control": "no-cache", + "content-length": "0", + "host": "testapi.execute-api.us-west-2.amazonaws.com", + "user-agent": "python/7.37.3", + "x-amzn-trace-id": "Root=1-661d2e7e-5d132c9f61e28d8f3be17aad", + "x-forwarded-for": "79.191.34.167", + "x-forwarded-port": "443", + "x-forwarded-proto": "https" + }, + "queryStringParameters": { + "q": "1" + }, + "requestContext": { + "accountId": "123456789012", + "apiId": "testapi", + "domainName": "testapi.execute-api.us-west-2.amazonaws.com", + "domainPrefix": "testapi", + "http": { + "method": "GET", + "path": "/dev/test-lambda-proxy-v2/14", + "protocol": "HTTP/1.1", + "sourceIp": "79.191.34.167", + "userAgent": "python/7.37.3" + }, + "requestId": "WRQz0jBEvHcEMjw=", + "routeKey": "ANY /test-lambda-proxy-v2/{id}", + "stage": "dev", + "time": "15/Apr/2024:13:41:18 +0000", + "timeEpoch": 1713188478585 + }, + "pathParameters": { + "id": "14" + }, + "stageVariables": { + "environment": "dev" + }, + "isBase64Encoded": false +} diff --git a/tests/integration/contrib/aws/data/v3.0/http-api/events/lambda-proxy.json b/tests/integration/contrib/aws/data/v3.0/http-api/events/lambda-proxy.json new file mode 100644 index 00000000..48199e29 --- /dev/null +++ b/tests/integration/contrib/aws/data/v3.0/http-api/events/lambda-proxy.json @@ -0,0 +1,97 @@ +{ + "version": "1.0", + "resource": "/test-lambda-proxy/{id}", + "path": "/dev/test-lambda-proxy/13", + "httpMethod": "GET", + "headers": { + "Content-Length": "0", + "Host": "testapi.execute-api.us-west-2.amazonaws.com", + "User-Agent": "python/7.37.3", + "X-Amzn-Trace-Id": "Root=1-661d2e89-5ee35c1b508c257713c5b594", + "X-Forwarded-For": "79.191.34.167", + "X-Forwarded-Port": "443", + "X-Forwarded-Proto": "https", + "accept": "*/*", + "accept-encoding": "gzip, deflate, br", + "cache-control": "no-cache" + }, + "multiValueHeaders": { + "Content-Length": [ + "0" + ], + "Host": [ + "testapi.execute-api.us-west-2.amazonaws.com" + ], + "User-Agent": [ + "python/7.37.3" + ], + "X-Amzn-Trace-Id": [ + "Root=1-661d2e89-5ee35c1b508c257713c5b594" + ], + "X-Forwarded-For": [ + "79.191.34.167" + ], + "X-Forwarded-Port": [ + "443" + ], + "X-Forwarded-Proto": [ + "https" + ], + "accept": [ + "*/*" + ], + "accept-encoding": [ + "gzip, deflate, br" + ], + "cache-control": [ + "no-cache" + ] + }, + "queryStringParameters": { + "q": "1" + }, + "multiValueQueryStringParameters": { + "q": [ + "1" + ] + }, + "requestContext": { + "accountId": "123456789012", + "apiId": "testapi", + "domainName": "testapi.execute-api.us-west-2.amazonaws.com", + "domainPrefix": "testapi", + "extendedRequestId": "WRQ1liQfvHcEMoQ=", + "httpMethod": "GET", + "identity": { + "accessKey": null, + "accountId": null, + "caller": null, + "cognitoAmr": null, + "cognitoAuthenticationProvider": null, + "cognitoAuthenticationType": null, + "cognitoIdentityId": null, + "cognitoIdentityPoolId": null, + "principalOrgId": null, + "sourceIp": "79.191.34.167", + "user": null, + "userAgent": "python/7.37.3", + "userArn": null + }, + "path": "/dev/test-lambda-proxy/13", + "protocol": "HTTP/1.1", + "requestId": "WRQ1liQfvHcEMoQ=", + "requestTime": "15/Apr/2024:13:41:29 +0000", + "requestTimeEpoch": 1713188489870, + "resourceId": "ANY /test-lambda-proxy/{id}", + "resourcePath": "/test-lambda-proxy/{id}", + "stage": "dev" + }, + "pathParameters": { + "id": "13" + }, + "stageVariables": { + "environment": "dev" + }, + "body": null, + "isBase64Encoded": false +} \ No newline at end of file diff --git a/tests/integration/contrib/aws/data/v3.0/http-api/http_api_with_apig_ext.yaml b/tests/integration/contrib/aws/data/v3.0/http-api/http_api_with_apig_ext.yaml new file mode 100644 index 00000000..de15793a --- /dev/null +++ b/tests/integration/contrib/aws/data/v3.0/http-api/http_api_with_apig_ext.yaml @@ -0,0 +1,104 @@ +openapi: "3.0.1" +info: + title: "test-http-api" + version: "2024-04-15 20:45:05UTC" +servers: +- url: "https://testapi.execute-api.us-west-2.amazonaws.com/{basePath}" + variables: + basePath: + default: "dev" +paths: + /test-http-proxy/{proxy+}: + parameters: + - name: "proxy+" + in: "path" + description: "Generated path parameter for proxy+" + required: true + schema: + type: "string" + x-amazon-apigateway-any-method: + responses: + default: + description: "Default response for ANY /test-http-proxy/{proxy+}" + x-amazon-apigateway-integration: + payloadFormatVersion: "1.0" + type: "http_proxy" + httpMethod: "ANY" + uri: "https://testlambdafunction.lambda-url.us-west-2.on.aws/{proxy}" + connectionType: "INTERNET" + /test-lambda-proxy-v2/{id}: + parameters: + - name: "id" + in: "path" + description: "Generated path parameter for id" + required: true + schema: + type: "string" + x-amazon-apigateway-any-method: + responses: + default: + description: "Default response for ANY /test-lambda-proxy-v2/{id}" + x-amazon-apigateway-integration: + payloadFormatVersion: "2.0" + type: "aws_proxy" + httpMethod: "POST" + uri: "arn:aws:apigateway:us-west-2:lambda:path/2015-03-31/functions/arn:aws:lambda:us-west-2:123456789012:function:test-openapi-func/invocations" + connectionType: "INTERNET" + timeoutInMillis: 30000 + /test-lambda-proxy/{id}: + parameters: + - name: "id" + in: "path" + description: "Generated path parameter for id" + required: true + schema: + type: "string" + x-amazon-apigateway-any-method: + responses: + default: + description: "Default response for ANY /test-lambda-proxy/{id}" + x-amazon-apigateway-integration: + payloadFormatVersion: "1.0" + type: "aws_proxy" + httpMethod: "POST" + uri: "arn:aws:apigateway:us-west-2:lambda:path/2015-03-31/functions/arn:aws:lambda:us-west-2:123456789012:function:test-openapi-func/invocations" + connectionType: "INTERNET" + timeoutInMillis: 30000 + /test-http-proxy: + get: + responses: + default: + description: "Default response for GET /test-http-proxy" + x-amazon-apigateway-integration: + requestParameters: + append:header.testing: "testing" + payloadFormatVersion: "1.0" + type: "http_proxy" + httpMethod: "ANY" + uri: "https://testlambdafunction.lambda-url.us-west-2.on.aws" + connectionType: "INTERNET" + /test-lambda-proxy: + get: + responses: + default: + description: "Default response for GET /test-lambda-proxy" + x-amazon-apigateway-integration: + payloadFormatVersion: "1.0" + type: "aws_proxy" + httpMethod: "POST" + uri: "arn:aws:apigateway:us-west-2:lambda:path/2015-03-31/functions/arn:aws:lambda:us-west-2:123456789012:function:test-openapi-func/invocations" + connectionType: "INTERNET" + timeoutInMillis: 30000 + /test-lambda-proxy-v2: + get: + responses: + default: + description: "Default response for GET /test-lambda-proxy-v2" + x-amazon-apigateway-integration: + payloadFormatVersion: "2.0" + type: "aws_proxy" + httpMethod: "POST" + uri: "arn:aws:apigateway:us-west-2:lambda:path/2015-03-31/functions/arn:aws:lambda:us-west-2:123456789012:function:test-openapi-func/invocations" + connectionType: "INTERNET" + timeoutInMillis: 30000 +x-amazon-apigateway-importexport-version: "1.0" diff --git a/tests/integration/contrib/aws/test_aws_validation.py b/tests/integration/contrib/aws/test_aws_validation.py new file mode 100644 index 00000000..2e4c6898 --- /dev/null +++ b/tests/integration/contrib/aws/test_aws_validation.py @@ -0,0 +1,81 @@ +import json +import pytest + +from openapi_core import Config +from openapi_core import OpenAPI +from openapi_core.contrib.aws import APIGatewayPathFinder +from openapi_core.contrib.aws import APIGatewayAWSProxyOpenAPIRequest +from openapi_core.contrib.aws import APIGatewayAWSProxyV2OpenAPIRequest +from openapi_core.contrib.aws import APIGatewayHTTPProxyOpenAPIRequest +from openapi_core.datatypes import Parameters + + +class BaseTestAWSProject: + api_key = "12345" + + @property + def api_key_encoded(self): + api_key_bytes = self.api_key.encode("utf8") + api_key_bytes_enc = b64encode(api_key_bytes) + return str(api_key_bytes_enc, "utf8") + + +class TestHTTPAPI(BaseTestAWSProject): + + @pytest.fixture + def openapi(self, content_factory): + content, _ = content_factory.from_file("contrib/aws/data/v3.0/http-api/http_api_with_apig_ext.yaml") + config = Config( + path_finder_cls=APIGatewayPathFinder, + ) + return OpenAPI.from_dict(content, config=config) + + def test_lambda_proxy_v1_event(self, openapi, content_factory): + event, _ = content_factory.from_file("contrib/aws/data/v3.0/http-api/events/lambda-proxy.json") + + openapi_request = APIGatewayAWSProxyOpenAPIRequest(event) + result = openapi.unmarshal_request(openapi_request) + + assert not result.errors + assert not result.body + assert result.parameters == Parameters( + path={"id": "13"} + ) + + def test_lambda_proxy_v2_event(self, openapi, content_factory): + event, _ = content_factory.from_file("contrib/aws/data/v3.0/http-api/events/lambda-proxy-v2.json") + + openapi_request = APIGatewayAWSProxyV2OpenAPIRequest(event) + result = openapi.unmarshal_request(openapi_request) + + assert not result.errors + assert not result.body + assert result.parameters == Parameters( + path={"id": "14"} + ) + + def test_http_proxy_event(self, openapi, content_factory): + event, _ = content_factory.from_file("contrib/aws/data/v3.0/http-api/events/http-proxy.json") + + openapi_request = APIGatewayHTTPProxyOpenAPIRequest(event, base_path="/test-http-proxy") + result = openapi.unmarshal_request(openapi_request) + + assert not result.errors + assert not result.body + assert result.parameters == Parameters() + + @pytest.mark.xfail( + reason="greedy path variable to catch all requests not supported", + strict=True, + ) + def test_http_proxy_catch_all(self, openapi, content_factory): + event, _ = content_factory.from_file("contrib/aws/data/v3.0/http-api/events/http-proxy-catch-all.json") + + openapi_request = APIGatewayHTTPProxyOpenAPIRequest(event, base_path="/test-http-proxy") + result = openapi.unmarshal_request(openapi_request) + + assert not result.errors + assert not result.body + assert result.parameters == Parameters( + path={"proxy": "12"} + )