Skip to content

Commit 70338b7

Browse files
committed
AWS API Gateway with Amazon Lambda integrations support
1 parent 059dd73 commit 70338b7

17 files changed

+979
-114
lines changed

docs/integrations/aws.rst

+70
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
Amazon API Gateway
2+
==================
3+
4+
This section describes integration with `Amazon API Gateway <https://aws.amazon.com/api-gateway/>`__.
5+
6+
It is useful for:
7+
8+
* `AWS Proxy integrations (i.e. AWS Lambda) for HTTP API <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).
9+
* `HTTP Proxy integrations for HTTP API <https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-develop-integrations-http.html>` where HTTP service handle events from API Gateway.
10+
* `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).
11+
12+
ANY method
13+
----------
14+
15+
Amazon API Gateway defines special ``ANY`` method that catches all HTTP methods. It is specified as `x-amazon-apigateway-any-method <https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-swagger-extensions-any-method.html>`__ OpenAPI extension. The extension is handled within custom path finder and can be used by setting ``path_finder_cls`` to be ``APIGatewayPathFinder``:
16+
17+
.. code-block:: python
18+
:emphasize-lines: 1,4
19+
20+
from openapi_core.contrib.aws import APIGatewayPathFinder
21+
22+
config = Config(
23+
path_finder_cls=APIGatewayPathFinder,
24+
)
25+
openapi = OpenAPI.from_file_path('openapi.json', config=config)
26+
27+
Low level
28+
---------
29+
30+
The integration defines classes useful for low level integration.
31+
32+
AWS Proxy event
33+
^^^^^^^^^^^^^^^
34+
35+
Use ``APIGatewayAWSProxyV2OpenAPIRequest`` to create OpenAPI request from an API Gateway event (format version 2.0):
36+
37+
.. code-block:: python
38+
39+
from openapi_core.contrib.aws import APIGatewayAWSProxyV2OpenAPIRequest
40+
41+
def handler(event, context):
42+
openapi_request = APIGatewayAWSProxyV2OpenAPIRequest(event)
43+
result = openapi.unmarshal_request(openapi_request)
44+
return {
45+
"statusCode": 200,
46+
"body": "Hello world",
47+
}
48+
49+
If you use format version 1.0, then import and use ``APIGatewayAWSProxyOpenAPIRequest``.
50+
51+
Response
52+
^^^^^^^^
53+
54+
Use ``APIGatewayEventV2ResponseOpenAPIResponse`` to create OpenAPI response from API Gateway event (format version 2.0) response:
55+
56+
.. code-block:: python
57+
58+
from openapi_core.contrib.aws import APIGatewayEventV2ResponseOpenAPIResponse
59+
60+
def handler(event, context):
61+
openapi_request = APIGatewayEventV2OpenAPIRequest(event)
62+
response = {
63+
"statusCode": 200,
64+
"body": "Hello world",
65+
}
66+
openapi_response = APIGatewayEventV2ResponseOpenAPIResponse(response)
67+
result = openapi.unmarshal_response(openapi_request, openapi_response)
68+
return response
69+
70+
If you use format version 1.0, then import and use ``APIGatewayEventResponseOpenAPIResponse``.

docs/integrations/index.rst

+1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ Openapi-core integrates with your popular libraries and frameworks. Each integra
66
.. toctree::
77
:maxdepth: 1
88

9+
aws
910
aiohttp
1011
bottle
1112
django

openapi_core/contrib/aws/__init__.py

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

openapi_core/contrib/aws/datatypes.py

+104
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
from typing import Dict
2+
from typing import List
3+
from typing import Literal
4+
from typing import Optional
5+
6+
from pydantic import Field
7+
from pydantic.dataclasses import dataclass
8+
9+
API_GATEWAY_EVENT_CONFIG = dict(extra="allow")
10+
11+
12+
@dataclass(frozen=True)
13+
class APIGatewayEventRequestContext:
14+
"""AWS API Gateway event request context"""
15+
model_config = API_GATEWAY_EVENT_CONFIG
16+
17+
resourceId: str
18+
19+
20+
@dataclass(frozen=True)
21+
class APIGatewayEvent:
22+
"""AWS API Gateway event"""
23+
model_config = API_GATEWAY_EVENT_CONFIG
24+
25+
headers: Dict[str, str]
26+
27+
path: str
28+
httpMethod: str
29+
resource: str
30+
requestContext: APIGatewayEventRequestContext
31+
32+
queryStringParameters: Optional[Dict[str, str]] = None
33+
isBase64Encoded: Optional[bool] = None
34+
body: Optional[str] = None
35+
pathParameters: Optional[Dict[str, str]] = None
36+
stageVariables: Optional[Dict[str, str]] = None
37+
38+
multiValueHeaders: Optional[Dict[str, List[str]]] = None
39+
version: Optional[str] = "1.0"
40+
multiValueQueryStringParameters: Optional[Dict[str, List[str]]] = None
41+
42+
43+
@dataclass(frozen=True)
44+
class APIGatewayEventV2Http:
45+
"""AWS API Gateway event v2 HTTP"""
46+
model_config = API_GATEWAY_EVENT_CONFIG
47+
48+
method: str
49+
path: str
50+
51+
52+
@dataclass(frozen=True)
53+
class APIGatewayEventV2RequestContext:
54+
"""AWS API Gateway event v2 request context"""
55+
model_config = API_GATEWAY_EVENT_CONFIG
56+
57+
http: APIGatewayEventV2Http
58+
59+
60+
@dataclass(frozen=True)
61+
class APIGatewayEventV2:
62+
"""AWS API Gateway event v2"""
63+
model_config = API_GATEWAY_EVENT_CONFIG
64+
65+
headers: Dict[str, str]
66+
67+
version: Literal["2.0"]
68+
routeKey: str
69+
rawPath: str
70+
rawQueryString: str
71+
requestContext: APIGatewayEventV2RequestContext
72+
73+
queryStringParameters: Optional[Dict[str, str]] = None
74+
isBase64Encoded: Optional[bool] = None
75+
body: Optional[str] = None
76+
pathParameters: Optional[Dict[str, str]] = None
77+
stageVariables: Optional[Dict[str, str]] = None
78+
79+
cookies: Optional[List[str]] = None
80+
81+
82+
@dataclass(frozen=True)
83+
class APIGatewayEventResponse:
84+
"""AWS API Gateway event response"""
85+
model_config = API_GATEWAY_EVENT_CONFIG
86+
87+
body: str
88+
isBase64Encoded: bool
89+
statusCode: int
90+
headers: Dict[str, str]
91+
multiValueHeaders: Dict[str, List[str]]
92+
93+
94+
@dataclass(frozen=True)
95+
class APIGatewayEventV2Response:
96+
"""AWS API Gateway event v2 response"""
97+
model_config = API_GATEWAY_EVENT_CONFIG
98+
99+
body: str
100+
isBase64Encoded: bool = False
101+
statusCode: int = 200
102+
headers: Dict[str, str] = Field(
103+
default_factory=lambda: {"content-type": "application/json"}
104+
)

openapi_core/contrib/aws/finders.py

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

openapi_core/contrib/aws/requests.py

+174
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
from typing import Dict
2+
from typing import Optional
3+
from typing import Tuple
4+
from urllib.parse import urljoin
5+
6+
from werkzeug.datastructures import Headers
7+
from werkzeug.datastructures import ImmutableMultiDict
8+
9+
from openapi_core.contrib.aws.datatypes import APIGatewayEvent
10+
from openapi_core.contrib.aws.datatypes import APIGatewayEventV2
11+
from openapi_core.contrib.aws.types import APIGatewayEventPayload
12+
from openapi_core.contrib.aws.util import parse_forwarded
13+
from openapi_core.datatypes import RequestParameters
14+
15+
16+
class APIGatewayAWSProxyOpenAPIRequest:
17+
"""
18+
API Gateway AWS proxy event payload OpenAPI request.
19+
20+
Designed to be used with API Gateway REST API specification exports for
21+
integrations that use event v1 payload. Uses API Gateway event v1
22+
requestContext's resourceId. Requires APIGatewayPathFinder to resolve ANY methods.
23+
"""
24+
25+
def __init__(self, payload: APIGatewayEventPayload):
26+
self.event = APIGatewayEvent(**payload)
27+
28+
self.parameters = RequestParameters(
29+
path=self.path_params,
30+
query=ImmutableMultiDict(self.query_params),
31+
header=Headers(self.event.headers),
32+
cookie=ImmutableMultiDict(),
33+
)
34+
35+
@property
36+
def resource_id(self) -> Tuple[str, str]:
37+
return self.event.requestContext.resourceId.split(" ")
38+
39+
@property
40+
def path_params(self) -> Dict[str, str]:
41+
params = self.event.pathParameters
42+
if params is None:
43+
return {}
44+
return params
45+
46+
@property
47+
def query_params(self) -> Dict[str, str]:
48+
params = self.event.queryStringParameters
49+
if params is None:
50+
return {}
51+
return params
52+
53+
@property
54+
def proto(self) -> str:
55+
return self.event.headers.get("X-Forwarded-Proto", "https")
56+
57+
@property
58+
def host(self) -> str:
59+
return self.event.headers["Host"]
60+
61+
@property
62+
def host_url(self) -> str:
63+
return "://".join([self.proto, self.host])
64+
65+
@property
66+
def path(self) -> str:
67+
return self.event.path
68+
69+
@property
70+
def method(self) -> str:
71+
return self.resource_id[0].lower()
72+
73+
@property
74+
def body(self) -> Optional[str]:
75+
return self.event.body
76+
77+
@property
78+
def content_type(self) -> str:
79+
return self.event.headers.get("Content-Type", "")
80+
81+
@property
82+
def path_pattern(self):
83+
return self.resource_id[1]
84+
85+
86+
class APIGatewayAWSProxyV2OpenAPIRequest:
87+
"""
88+
API Gateway AWS Proxy event v2 payload OpenAPI request.
89+
90+
Designed to be used with API Gateway HTTP API specification exports for
91+
integrations that use event v2 payload. Uses API Gateway event v2 routeKey
92+
and rawPath data. Requires APIGatewayPathFinder to resolve ANY methods.
93+
94+
.. note::
95+
API Gateway HTTP APIs don't support request validation
96+
"""
97+
98+
def __init__(self, payload: APIGatewayEventPayload):
99+
self.event = APIGatewayEventV2(**payload)
100+
101+
self.parameters = RequestParameters(
102+
path=self.path_params,
103+
query=ImmutableMultiDict(self.query_params),
104+
header=Headers(self.event.headers),
105+
cookie=ImmutableMultiDict(),
106+
)
107+
108+
@property
109+
def path_params(self) -> Dict[str, str]:
110+
if self.event.pathParameters is None:
111+
return {}
112+
return self.event.pathParameters
113+
114+
@property
115+
def query_params(self) -> Dict[str, str]:
116+
if self.event.queryStringParameters is None:
117+
return {}
118+
return self.event.queryStringParameters
119+
120+
@property
121+
def proto(self) -> str:
122+
return self.event.headers.get("x-forwarded-proto", "https")
123+
124+
@property
125+
def host(self) -> str:
126+
# use Forwarded header if available
127+
if "forwarded" in self.event.headers:
128+
forwarded = parse_forwarded(self.event.headers["forwarded"])
129+
if "host" in forwarded:
130+
return forwarded["host"]
131+
return self.event.headers["host"]
132+
133+
@property
134+
def host_url(self) -> str:
135+
return "://".join([self.proto, self.host])
136+
137+
@property
138+
def path(self) -> str:
139+
return self.event.rawPath
140+
141+
@property
142+
def method(self) -> str:
143+
return self.event.routeKey.split(" ")[0].lower()
144+
145+
@property
146+
def body(self) -> Optional[str]:
147+
return self.event.body
148+
149+
@property
150+
def content_type(self) -> str:
151+
return self.event.headers.get("content-type", "")
152+
153+
154+
class APIGatewayHTTPProxyOpenAPIRequest(APIGatewayAWSProxyV2OpenAPIRequest):
155+
"""
156+
API Gateway HTTP proxy integration event payload OpenAPI request.
157+
158+
Uses http integration path and method data.
159+
160+
NOTE: If you use HTTP integration not in root path then you need to provide the base path
161+
otherwise it won't find the correct path because it is not send with event.
162+
"""
163+
164+
def __init__(self, payload: APIGatewayEventPayload, base_path: str = "/"):
165+
super().__init__(payload)
166+
self.base_path = base_path
167+
168+
@property
169+
def path(self) -> str:
170+
return urljoin(self.base_path, self.event.requestContext.http.path.lstrip('/'))
171+
172+
@property
173+
def method(self) -> str:
174+
return self.event.requestContext.http.method.lower()

0 commit comments

Comments
 (0)