Skip to content

Commit 621be8d

Browse files
committed
AWS API Gateway with Amazon Lambda integrations support
1 parent 0898d87 commit 621be8d

18 files changed

+976
-165
lines changed

docs/integrations.rst

+53
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,59 @@ Integrations
33

44
Openapi-core integrates with your popular libraries and frameworks. Each integration offers different levels of integration that help validate and unmarshal your request and response data.
55

6+
Amazon API Gateway
7+
------------------
8+
9+
This section describes integration with `Amazon API Gateway <https://aws.amazon.com/api-gateway/>`__.
10+
11+
It is useful for:
12+
* `AWS Lambda integrations <https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-develop-integrations-lambda.html>`__ where Lambda functions handle events from API Gateway (Amazon API Gateway event format version 1.0 and 2.0).
13+
* `AWS Lambda function URLs <https://docs.aws.amazon.com/lambda/latest/dg/lambda-urls.html>` where Lambda functions handle events from dedicated HTTP(S) endpoint (Amazon API Gateway event format version 2.0).
14+
15+
Low level
16+
~~~~~~~~~
17+
18+
You can use ``APIGatewayEventV2OpenAPIRequest`` as an API Gateway event (format version 2.0) request factory:
19+
20+
.. code-block:: python
21+
22+
from openapi_core import unmarshal_request
23+
from openapi_core.contrib.aws import APIGatewayEventV2OpenAPIRequest
24+
25+
openapi_request = APIGatewayEventV2OpenAPIRequest(event)
26+
result = unmarshal_request(openapi_request, spec=spec)
27+
28+
If you use format version 1.0, then import and use ``APIGatewayEventOpenAPIRequest`` as an API Gateway event (format version 1.0) request factory.
29+
30+
You can use ``APIGatewayEventV2ResponseOpenAPIResponse`` as an API Gateway event (format version 2.0) response factory:
31+
32+
.. code-block:: python
33+
34+
from openapi_core import unmarshal_response
35+
from openapi_core.contrib.aws import APIGatewayEventV2ResponseOpenAPIResponse
36+
37+
openapi_response = APIGatewayEventV2ResponseOpenAPIResponse(response)
38+
result = unmarshal_response(openapi_request, openapi_response, spec=spec)
39+
40+
If you use format version 1.0, then import and use ``APIGatewayEventResponseOpenAPIResponse`` as an API Gateway event (format version 1.0) response factory.
41+
42+
ANY method
43+
~~~~~~~~~~
44+
45+
API Gateway have special ``ANY`` method that catches all HTTP methods. It's specified as `x-amazon-apigateway-any-method <https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-swagger-extensions-any-method.html>`__ OpenAPI extension. If you use the extension, you want to define ``path_finder_cls`` to be ``APIGatewayPathFinder``:
46+
47+
.. code-block:: python
48+
49+
from openapi_core.contrib.aws import APIGatewayPathFinder
50+
51+
result = unmarshal_response(
52+
openapi_request,
53+
openapi_response,
54+
spec=spec,
55+
path_finder_cls=APIGatewayPathFinder,
56+
)
57+
58+
659
Bottle
760
------
861

openapi_core/contrib/aws/__init__.py

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
"""OpenAPI core contrib aws module"""
2+
from openapi_core.contrib.aws.finders import APIGatewayPathFinder
3+
from openapi_core.contrib.aws.requests import APIGatewayEventOpenAPIRequest
4+
from openapi_core.contrib.aws.requests import APIGatewayEventV2OpenAPIRequest
5+
from openapi_core.contrib.aws.responses import (
6+
APIGatewayEventResponseOpenAPIResponse,
7+
)
8+
from openapi_core.contrib.aws.responses import (
9+
APIGatewayEventV2ResponseOpenAPIResponse,
10+
)
11+
12+
__all__ = [
13+
"APIGatewayEventOpenAPIRequest",
14+
"APIGatewayEventV2OpenAPIRequest",
15+
"APIGatewayEventResponseOpenAPIResponse",
16+
"APIGatewayEventV2ResponseOpenAPIResponse",
17+
"APIGatewayPathFinder",
18+
]

openapi_core/contrib/aws/datatypes.py

+74
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
from typing import Dict
2+
from typing import List
3+
from typing import Optional
4+
5+
from pydantic import Field
6+
from pydantic.dataclasses import dataclass
7+
8+
9+
class APIGatewayEventConfig:
10+
extra = "allow"
11+
12+
13+
@dataclass(config=APIGatewayEventConfig, frozen=True)
14+
class APIGatewayEvent:
15+
"""AWS API Gateway event"""
16+
17+
headers: Dict[str, str]
18+
19+
path: str
20+
httpMethod: str
21+
resource: str
22+
23+
queryStringParameters: Optional[Dict[str, str]] = None
24+
isBase64Encoded: Optional[bool] = None
25+
body: Optional[str] = None
26+
pathParameters: Optional[Dict[str, str]] = None
27+
stageVariables: Optional[Dict[str, str]] = None
28+
29+
multiValueHeaders: Optional[Dict[str, List[str]]] = None
30+
version: Optional[str] = "1.0"
31+
multiValueQueryStringParameters: Optional[Dict[str, List[str]]] = None
32+
33+
34+
@dataclass(config=APIGatewayEventConfig, frozen=True)
35+
class APIGatewayEventV2:
36+
"""AWS API Gateway event v2"""
37+
38+
headers: Dict[str, str]
39+
40+
version: str
41+
routeKey: str
42+
rawPath: str
43+
rawQueryString: str
44+
45+
queryStringParameters: Optional[Dict[str, str]] = None
46+
isBase64Encoded: Optional[bool] = None
47+
body: Optional[str] = None
48+
pathParameters: Optional[Dict[str, str]] = None
49+
stageVariables: Optional[Dict[str, str]] = None
50+
51+
cookies: Optional[List[str]] = None
52+
53+
54+
@dataclass(config=APIGatewayEventConfig, frozen=True)
55+
class APIGatewayEventResponse:
56+
"""AWS API Gateway event response"""
57+
58+
body: str
59+
isBase64Encoded: bool
60+
statusCode: int
61+
headers: Dict[str, str]
62+
multiValueHeaders: Dict[str, List[str]]
63+
64+
65+
@dataclass(config=APIGatewayEventConfig, frozen=True)
66+
class APIGatewayEventV2Response:
67+
"""AWS API Gateway event v2 response"""
68+
69+
body: str
70+
isBase64Encoded: bool = False
71+
statusCode: int = 200
72+
headers: Dict[str, str] = Field(
73+
default_factory=lambda: {"content-type": "application/json"}
74+
)

openapi_core/contrib/aws/finders.py

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
from openapi_core.templating.paths.finders import APICallPathFinder
2+
from openapi_core.templating.paths.iterators import AnyMethodOperationsIterator
3+
4+
5+
class APIGatewayPathFinder(APICallPathFinder):
6+
operations_iterator = AnyMethodOperationsIterator(
7+
any_method="x-amazon-apigateway-any-method",
8+
)

openapi_core/contrib/aws/requests.py

+109
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
from typing import Dict
2+
from typing import Optional
3+
4+
from werkzeug.datastructures import Headers
5+
from werkzeug.datastructures import ImmutableMultiDict
6+
7+
from openapi_core.contrib.aws.datatypes import APIGatewayEvent
8+
from openapi_core.contrib.aws.datatypes import APIGatewayEventV2
9+
from openapi_core.contrib.aws.types import APIGatewayEventPayload
10+
from openapi_core.datatypes import RequestParameters
11+
12+
13+
class APIGatewayEventOpenAPIRequest:
14+
"""
15+
Converts an API Gateway event payload to an OpenAPI request
16+
"""
17+
18+
def __init__(self, payload: APIGatewayEventPayload):
19+
self.event = APIGatewayEvent(**payload)
20+
21+
self.parameters = RequestParameters(
22+
query=ImmutableMultiDict(self.query_params),
23+
header=Headers(self.event.headers),
24+
cookie=ImmutableMultiDict(),
25+
)
26+
27+
@property
28+
def query_params(self) -> Dict[str, str]:
29+
params = self.event.queryStringParameters
30+
if params is None:
31+
return {}
32+
return params
33+
34+
@property
35+
def proto(self) -> str:
36+
return self.event.headers.get("X-Forwarded-Proto", "https")
37+
38+
@property
39+
def host(self) -> str:
40+
return self.event.headers["Host"]
41+
42+
@property
43+
def host_url(self) -> str:
44+
return "://".join([self.proto, self.host])
45+
46+
@property
47+
def path(self) -> str:
48+
return self.event.resource
49+
50+
@property
51+
def method(self) -> str:
52+
return self.event.httpMethod.lower()
53+
54+
@property
55+
def body(self) -> Optional[str]:
56+
return self.event.body
57+
58+
@property
59+
def mimetype(self) -> str:
60+
return self.event.headers.get("Content-Type", "")
61+
62+
63+
class APIGatewayEventV2OpenAPIRequest:
64+
"""
65+
Converts an API Gateway event v2 payload to an OpenAPI request
66+
"""
67+
68+
def __init__(self, payload: APIGatewayEventPayload):
69+
self.event = APIGatewayEventV2(**payload)
70+
71+
self.parameters = RequestParameters(
72+
query=ImmutableMultiDict(self.query_params),
73+
header=Headers(self.event.headers),
74+
cookie=ImmutableMultiDict(),
75+
)
76+
77+
@property
78+
def query_params(self) -> Dict[str, str]:
79+
if self.event.queryStringParameters is None:
80+
return {}
81+
return self.event.queryStringParameters
82+
83+
@property
84+
def proto(self) -> str:
85+
return self.event.headers.get("x-forwarded-proto", "https")
86+
87+
@property
88+
def host(self) -> str:
89+
return self.event.headers["host"]
90+
91+
@property
92+
def host_url(self) -> str:
93+
return "://".join([self.proto, self.host])
94+
95+
@property
96+
def path(self) -> str:
97+
return self.event.rawPath
98+
99+
@property
100+
def method(self) -> str:
101+
return self.event.routeKey.lower()
102+
103+
@property
104+
def body(self) -> Optional[str]:
105+
return self.event.body
106+
107+
@property
108+
def mimetype(self) -> str:
109+
return self.event.headers.get("content-type", "")

openapi_core/contrib/aws/responses.py

+83
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
from json import dumps
2+
from typing import Union
3+
4+
from werkzeug.datastructures import Headers
5+
6+
from openapi_core.contrib.aws.datatypes import APIGatewayEventResponse
7+
from openapi_core.contrib.aws.datatypes import APIGatewayEventV2Response
8+
from openapi_core.contrib.aws.types import APIGatewayEventResponsePayload
9+
10+
APIGatewayEventV2ResponseType = Union[APIGatewayEventV2Response, dict, str]
11+
12+
13+
class APIGatewayEventResponseOpenAPIResponse:
14+
"""
15+
Converts an API Gateway event response payload to an OpenAPI request
16+
"""
17+
18+
def __init__(self, payload: APIGatewayEventResponsePayload):
19+
self.response = APIGatewayEventResponse(**payload)
20+
21+
@property
22+
def data(self) -> str:
23+
return self.response.body
24+
25+
@property
26+
def status_code(self) -> int:
27+
return self.response.statusCode
28+
29+
@property
30+
def headers(self) -> Headers:
31+
return Headers(self.response.headers)
32+
33+
@property
34+
def mimetype(self) -> str:
35+
content_type = self.response.headers.get("Content-Type", "")
36+
assert isinstance(content_type, str)
37+
return content_type
38+
39+
40+
class APIGatewayEventV2ResponseOpenAPIResponse:
41+
"""
42+
Converts an API Gateway event v2 response payload to an OpenAPI request
43+
"""
44+
45+
def __init__(self, payload: Union[APIGatewayEventResponsePayload, str]):
46+
if not isinstance(payload, dict):
47+
payload = self._construct_payload(payload)
48+
elif "statusCode" not in payload:
49+
body = dumps(payload)
50+
payload = self._construct_payload(body)
51+
52+
self.response = APIGatewayEventV2Response(**payload)
53+
54+
@staticmethod
55+
def _construct_payload(body: str) -> APIGatewayEventResponsePayload:
56+
return {
57+
"isBase64Encoded": False,
58+
"statusCode": 200,
59+
"headers": {
60+
"content-type": "application/json",
61+
},
62+
"body": body,
63+
}
64+
65+
@property
66+
def data(self) -> str:
67+
return self.response.body
68+
69+
@property
70+
def status_code(self) -> int:
71+
return self.response.statusCode
72+
73+
@property
74+
def headers(self) -> Headers:
75+
return Headers(self.response.headers)
76+
77+
@property
78+
def mimetype(self) -> str:
79+
content_type = self.response.headers.get(
80+
"content-type", "application/json"
81+
)
82+
assert isinstance(content_type, str)
83+
return content_type

openapi_core/contrib/aws/types.py

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
from typing import Any
2+
from typing import Dict
3+
4+
APIGatewayEventPayload = Dict[str, Any]
5+
APIGatewayEventResponsePayload = Dict[str, Any]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
from openapi_core.templating.paths.finders import APICallPathFinder
2+
from openapi_core.templating.paths.finders import WebhookPathFinder
3+
4+
__all__ = [
5+
"APICallPathFinder",
6+
"WebhookPathFinder",
7+
]

0 commit comments

Comments
 (0)