fastapi-aws 0.0.7__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- fastapi_aws/__init__.py +7 -0
- fastapi_aws/__main__.py +342 -0
- fastapi_aws/_version.py +21 -0
- fastapi_aws/authorizers.py +182 -0
- fastapi_aws/integrations.py +306 -0
- fastapi_aws/models.py +57 -0
- fastapi_aws/route.py +191 -0
- fastapi_aws/router.py +45 -0
- fastapi_aws-0.0.7.dist-info/METADATA +332 -0
- fastapi_aws-0.0.7.dist-info/RECORD +12 -0
- fastapi_aws-0.0.7.dist-info/WHEEL +5 -0
- fastapi_aws-0.0.7.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
from typing import Any, Dict, List
|
|
2
|
+
import json
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
from .route import register_integration
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@register_integration("mock")
|
|
9
|
+
def mock_integration(
|
|
10
|
+
uri: str, iam_arn: str, path_parameters: List[str], **kwargs
|
|
11
|
+
) -> Dict[str, Any]:
|
|
12
|
+
"""returns a mock integration which has a fixed response value
|
|
13
|
+
NB: this function should take parameters for the fixed responses.
|
|
14
|
+
NB: this can currently be used for a 'not implemented' response.
|
|
15
|
+
"""
|
|
16
|
+
return dict(
|
|
17
|
+
uri="",
|
|
18
|
+
integration_type="mock",
|
|
19
|
+
request_template={"statusCode": 200},
|
|
20
|
+
responses={
|
|
21
|
+
"default": {
|
|
22
|
+
"statusCode": 501,
|
|
23
|
+
"responseTemplates": {
|
|
24
|
+
"application/json": json.dumps({"status": "not implemented"})
|
|
25
|
+
},
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@register_integration("aws_lambda_uri")
|
|
32
|
+
def lambda_integration(
|
|
33
|
+
uri: str,
|
|
34
|
+
iam_arn: str,
|
|
35
|
+
integration_type="aws_proxy",
|
|
36
|
+
path_parameters: List[str] = None,
|
|
37
|
+
request_template: str = None,
|
|
38
|
+
**kwargs,
|
|
39
|
+
) -> Dict[str, Any]:
|
|
40
|
+
"""returns an aws integration description for calling lambdas from apigw.
|
|
41
|
+
|
|
42
|
+
NB: this return value includes strings relating to resource arns in terraform,
|
|
43
|
+
so the apigw deployment must load this function output and replace these placeholders.
|
|
44
|
+
|
|
45
|
+
The return value should look like:
|
|
46
|
+
"x-amazon-apigateway-integration": {
|
|
47
|
+
"uri": "${lambda_function_arn}",
|
|
48
|
+
"httpMethod": "POST",
|
|
49
|
+
"type": "aws",
|
|
50
|
+
"credentials": "${lambda_function_iam_arn}",
|
|
51
|
+
"requestTemplates": {
|
|
52
|
+
"application/json": json.dumps({
|
|
53
|
+
"body": "$input.json('$')",
|
|
54
|
+
"httpMethod": "POST",
|
|
55
|
+
"resource": "/",
|
|
56
|
+
"path": "/"
|
|
57
|
+
})
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
There is also a "responses" key which we should set to reformat the output, but do not yet.
|
|
61
|
+
The optional "path_parameters" parameter is a list of variable path elements
|
|
62
|
+
which are added to the requestTemplate:
|
|
63
|
+
"application/json": json.dumps({
|
|
64
|
+
"body": "$input.json('$')",
|
|
65
|
+
"httpMethod": "POST",
|
|
66
|
+
"resource": "/",
|
|
67
|
+
"path": "/"
|
|
68
|
+
"pathParameters": {...}
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
MB: the format of this pathParameters string is important, see the code for details
|
|
72
|
+
|
|
73
|
+
NB: use "type": "aws" to pass request.body directory, or "aws_proxy" to get request context
|
|
74
|
+
"""
|
|
75
|
+
if path_parameters:
|
|
76
|
+
if not isinstance(path_parameters, list):
|
|
77
|
+
raise ValueError("path_parameters must be a list of strings")
|
|
78
|
+
|
|
79
|
+
request_parameters = {
|
|
80
|
+
"integration.request.path.%s" % k: "method.request.path.%s" % k
|
|
81
|
+
for k in path_parameters
|
|
82
|
+
}
|
|
83
|
+
else:
|
|
84
|
+
request_parameters = None
|
|
85
|
+
|
|
86
|
+
if request_template:
|
|
87
|
+
request_template = {"application/json": request_template}
|
|
88
|
+
|
|
89
|
+
responses = {"default": {"statusCode": "200"}}
|
|
90
|
+
|
|
91
|
+
return dict(
|
|
92
|
+
uri=uri,
|
|
93
|
+
integration_type=integration_type,
|
|
94
|
+
http_method="POST",
|
|
95
|
+
credentials=iam_arn,
|
|
96
|
+
request_template=request_template,
|
|
97
|
+
request_parameters=request_parameters,
|
|
98
|
+
responses=responses,
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def step_function_integration_base(
|
|
103
|
+
uri: str,
|
|
104
|
+
sfn_arn: str,
|
|
105
|
+
iam_arn: str,
|
|
106
|
+
mapping_template: Dict[str, str],
|
|
107
|
+
) -> Dict[str, Any]:
|
|
108
|
+
"""returns an aws integration for sync invocation of a step function from apigw.
|
|
109
|
+
|
|
110
|
+
NB: the input to the step function is always the json serialized body object.
|
|
111
|
+
we not **not** pass through any path parameters at the moment.
|
|
112
|
+
|
|
113
|
+
NB: this return value includes strings relating to resource arns in terraform,
|
|
114
|
+
so the apigw deployment must load this function output and replace these placeholders.
|
|
115
|
+
|
|
116
|
+
The return value should look like:
|
|
117
|
+
"x-amazon-apigateway-integration": {
|
|
118
|
+
"uri": "arn:aws:apigateway:${region}:states:action/StartSyncExecution",
|
|
119
|
+
"httpMethod": "POST",
|
|
120
|
+
"type": "aws",
|
|
121
|
+
"credentials": "${sfn_invoke_iam_role_arn}",
|
|
122
|
+
"requestTemplates": {
|
|
123
|
+
"application/json": json.dumps({
|
|
124
|
+
"input": "$util.escapeJavaScript($input.json(\'$\'))",
|
|
125
|
+
"stateMachineArn": "${step_function_arn}",
|
|
126
|
+
"region": "${region}"
|
|
127
|
+
})
|
|
128
|
+
},
|
|
129
|
+
"responses": {
|
|
130
|
+
"default": {
|
|
131
|
+
"statusCode": "200",
|
|
132
|
+
"responseTemplates": {
|
|
133
|
+
"application/json": "#set($output = $util.parseJson($input.path('$.output')))\n$output.body"
|
|
134
|
+
},
|
|
135
|
+
}
|
|
136
|
+
},
|
|
137
|
+
}
|
|
138
|
+
},
|
|
139
|
+
NB: the format of this pathParameters string is important, see the code for details
|
|
140
|
+
"""
|
|
141
|
+
if mapping_template is None:
|
|
142
|
+
mapping_template = "$input.json('$')"
|
|
143
|
+
elif isinstance(mapping_template, dict):
|
|
144
|
+
mapping_template = json.dumps(mapping_template)
|
|
145
|
+
|
|
146
|
+
request_template = {
|
|
147
|
+
"application/json": json.dumps(
|
|
148
|
+
{
|
|
149
|
+
"input": mapping_template,
|
|
150
|
+
"stateMachineArn": sfn_arn,
|
|
151
|
+
"region": "${region}",
|
|
152
|
+
}
|
|
153
|
+
)
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
# FIXME: take response templates as parameters so we can handle errors nicely.
|
|
157
|
+
responses = {
|
|
158
|
+
"default": {
|
|
159
|
+
"statusCode": "200",
|
|
160
|
+
"responseTemplates": {
|
|
161
|
+
"application/json": "#set($output = $util.parseJson($input.path('$.output')))\n$output.body"
|
|
162
|
+
},
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return dict(
|
|
167
|
+
uri=uri,
|
|
168
|
+
integration_type="aws",
|
|
169
|
+
http_method="POST",
|
|
170
|
+
credentials=iam_arn,
|
|
171
|
+
request_template=request_template,
|
|
172
|
+
responses=responses,
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
@register_integration("aws_sfn_sync_arn")
|
|
177
|
+
def step_function_sync_integration(
|
|
178
|
+
sfn_arn: str,
|
|
179
|
+
iam_arn: str,
|
|
180
|
+
path_parameters: List[str] = None,
|
|
181
|
+
mapping_template: dict = None,
|
|
182
|
+
**kwargs,
|
|
183
|
+
) -> Dict[str, Any]:
|
|
184
|
+
"""returns an aws integration for sync invocation of a step function from apigw."""
|
|
185
|
+
|
|
186
|
+
return step_function_integration_base(
|
|
187
|
+
"arn:aws:apigateway:${region}:states:action/StartSyncExecution",
|
|
188
|
+
sfn_arn,
|
|
189
|
+
iam_arn,
|
|
190
|
+
mapping_template,
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
@register_integration("aws_sfn_arn")
|
|
195
|
+
def step_function_integration(
|
|
196
|
+
sfn_arn: str,
|
|
197
|
+
iam_arn: str,
|
|
198
|
+
path_parameters: List[str] = None,
|
|
199
|
+
mapping_template: dict = None,
|
|
200
|
+
**kwargs,
|
|
201
|
+
) -> Dict[str, Any]:
|
|
202
|
+
return step_function_integration_base(
|
|
203
|
+
"arn:aws:apigateway:${region}:states:action/StartExecution",
|
|
204
|
+
sfn_arn,
|
|
205
|
+
iam_arn,
|
|
206
|
+
mapping_template,
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
@register_integration("aws_s3_bucket")
|
|
211
|
+
def s3_integration(
|
|
212
|
+
bucket_name: str,
|
|
213
|
+
iam_arn: str,
|
|
214
|
+
path_parameters: List[str] = None,
|
|
215
|
+
http_method: str = "GET",
|
|
216
|
+
object_key: str = None,
|
|
217
|
+
**kwargs,
|
|
218
|
+
) -> Dict[str, Any]:
|
|
219
|
+
"""
|
|
220
|
+
Returns an AWS integration for S3 from API Gateway.
|
|
221
|
+
|
|
222
|
+
This allows API Gateway to interact with S3 objects via HTTP methods.
|
|
223
|
+
|
|
224
|
+
:param bucket_name: Name of the S3 bucket.
|
|
225
|
+
:param iam_arn: IAM role ARN to assume for the integration.
|
|
226
|
+
:param path_parameters: list of path parameters in the apigw request path; if `object_key` is None, the last value here is used as as the object key to lookup
|
|
227
|
+
:param http_method: HTTP method (GET, PUT, DELETE) to use for the integration.
|
|
228
|
+
:param object_key: [optional] fixed object key to return from the bucket
|
|
229
|
+
|
|
230
|
+
# TODO: path_parameters could be merged to allow multiple names
|
|
231
|
+
# TODO: object_key_prefix: allow a key prefix to be used for accessing objects by path parameter
|
|
232
|
+
|
|
233
|
+
Example OpenAPI integration:
|
|
234
|
+
|
|
235
|
+
"x-amazon-apigateway-integration": {
|
|
236
|
+
"uri": "arn:aws:apigateway:${region}:s3:path/{bucket}/{key}",
|
|
237
|
+
"httpMethod": "GET",
|
|
238
|
+
"type": "aws",
|
|
239
|
+
"credentials": "${iam_role_arn}",
|
|
240
|
+
"requestParameters": {
|
|
241
|
+
"integration.request.path.bucket": "bucket_name",
|
|
242
|
+
"integration.request.path.key": "method.request.path.key"
|
|
243
|
+
},
|
|
244
|
+
"responses": {
|
|
245
|
+
"default": {
|
|
246
|
+
"statusCode": "200",
|
|
247
|
+
"responseParameters": {
|
|
248
|
+
"method.response.header.Content-Type": "'application/octet-stream'"
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
"""
|
|
254
|
+
assert http_method in (
|
|
255
|
+
"GET",
|
|
256
|
+
"PUT",
|
|
257
|
+
"DELETE",
|
|
258
|
+
), "Invalid HTTP method for S3 integration"
|
|
259
|
+
|
|
260
|
+
# define the S3 integration URI using API Gateway's S3 service
|
|
261
|
+
# uri = f"arn:aws:apigateway:${{region}}:s3:path/{bucket_name}/{{key}}"
|
|
262
|
+
# FIXME: add the key parameter here to specify an object; but where should the value coem from?
|
|
263
|
+
# path_parameters? kwargs?
|
|
264
|
+
# NB: i think uri should be the bucket arn
|
|
265
|
+
uri = f"arn:aws:apigateway:${{region}}:s3:path/{bucket_name}"
|
|
266
|
+
|
|
267
|
+
if object_key:
|
|
268
|
+
uri = "/".join((uri, object_key))
|
|
269
|
+
elif path_parameters:
|
|
270
|
+
uri = "/".join([uri] + path_parameters)
|
|
271
|
+
else:
|
|
272
|
+
raise ValueError("expected one of: 'object_key', 'path_parameters'")
|
|
273
|
+
|
|
274
|
+
# apigw request parameters mapping
|
|
275
|
+
request_parameters = kwargs.get("request_parameters")
|
|
276
|
+
|
|
277
|
+
# response mapping (simple passthrough)
|
|
278
|
+
# FIXME: take the integration response content type to get the object content type
|
|
279
|
+
default_response_parameters = {
|
|
280
|
+
"default": {
|
|
281
|
+
"statusCode": "200",
|
|
282
|
+
# "responseParameters": {
|
|
283
|
+
# "method.response.header.Content-Type": "integration.response.header.Content-Type"
|
|
284
|
+
# },
|
|
285
|
+
},
|
|
286
|
+
"4xx": {"statusCode": "404"},
|
|
287
|
+
"403": {"statusCode": "404"},
|
|
288
|
+
"404": {"statusCode": "404"},
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
responses = kwargs.get("responses") or default_response_parameters
|
|
292
|
+
|
|
293
|
+
# generate apigw integration config
|
|
294
|
+
integration = dict(
|
|
295
|
+
uri=uri,
|
|
296
|
+
http_method=http_method,
|
|
297
|
+
integration_type="aws",
|
|
298
|
+
credentials=iam_arn,
|
|
299
|
+
request_parameters=request_parameters,
|
|
300
|
+
responses=responses,
|
|
301
|
+
)
|
|
302
|
+
|
|
303
|
+
if request_parameters:
|
|
304
|
+
integration["request_parameters"] = request_parameters
|
|
305
|
+
|
|
306
|
+
return integration
|
fastapi_aws/models.py
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""Pydantic models for the AWS API integration validation
|
|
2
|
+
"""
|
|
3
|
+
class IntegrationType(str, Enum):
|
|
4
|
+
AWS_PROXY = "aws_proxy"
|
|
5
|
+
AWS = "aws"
|
|
6
|
+
MOCK = "mock"
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Integration(BaseModel):
|
|
10
|
+
"""Baseclass for defining integrations
|
|
11
|
+
TODO: build the integrations with classes and then export to dict.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
type: IntegrationType
|
|
15
|
+
requestTemplates: Dict[str, Any]
|
|
16
|
+
responses: Dict[str, Any]
|
|
17
|
+
|
|
18
|
+
def model_dump(self) -> Dict[str, Any]:
|
|
19
|
+
return {"x-amazon-apigateway-integration": super().model_dump()}
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class LambdaJSONRequestTemplate(BaseModel):
|
|
23
|
+
body: str
|
|
24
|
+
httpMethod: str = "POST"
|
|
25
|
+
resource: str = "/"
|
|
26
|
+
path: str = "/"
|
|
27
|
+
pathParameters: Optional[Dict[str, str]] = None
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class MockIntegration(Integration):
|
|
31
|
+
type: IntegrationType = IntegrationType.MOCK
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class ServiceIntegration(Integration):
|
|
35
|
+
uri: str = Field(..., description="arn of the called resource")
|
|
36
|
+
credentials: str = Field(..., description="arn of the IAM used to invoke the resource")
|
|
37
|
+
httpMethod: str = "POST"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class LambdaIntegration(ServiceIntegration):
|
|
41
|
+
type: IntegrationType = IntegrationType.AWS_PROXY
|
|
42
|
+
# uri must be lambda arn
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class StepFunctionIntegration(ServiceIntegration):
|
|
46
|
+
type: IntegrationType = IntegrationType.AWS
|
|
47
|
+
# uri: "arn:aws:apigateway:${region}:states:action/StartSyncExecution"
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class NotImplementedIntegration(MockIntegration):
|
|
51
|
+
requestTemplates: Dict[str, Any] = {"application/json": {"statusCode": 200}}
|
|
52
|
+
responses: Dict[str, Any] = {
|
|
53
|
+
"default": {
|
|
54
|
+
"statusCode": "200",
|
|
55
|
+
"responseTemplates": {"application/json": '{"status": "not implemented"}'},
|
|
56
|
+
}
|
|
57
|
+
}
|
fastapi_aws/route.py
ADDED
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
from fastapi.routing import APIRoute
|
|
2
|
+
from typing import Any, Callable, List
|
|
3
|
+
from string import Formatter
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def register_integration(service_name: str):
|
|
7
|
+
"""Decorator to register integration methods dynamically in AWSAPIRoute"""
|
|
8
|
+
|
|
9
|
+
def decorator(func):
|
|
10
|
+
if not hasattr(AWSAPIRoute, "_integration_registry"):
|
|
11
|
+
AWSAPIRoute._integration_registry = {}
|
|
12
|
+
AWSAPIRoute._integration_registry[service_name] = func
|
|
13
|
+
return func
|
|
14
|
+
|
|
15
|
+
return decorator
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class AWSAPIRoute(APIRoute):
|
|
19
|
+
"""fastapi.apiroute derived class adapted for aws service integrations
|
|
20
|
+
This will provide an openapi json description of an aws service integration
|
|
21
|
+
for a route, allowing the openapi spec to be uploaded as an aws apigw rest
|
|
22
|
+
service automativicly.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
_integration_registry = {} # registered service integrations
|
|
26
|
+
|
|
27
|
+
def __init__(self, path: str, endpoint: Callable[..., Any], **kwargs: Any):
|
|
28
|
+
"""overload the APIRoute constructor
|
|
29
|
+
|
|
30
|
+
This has to happen because we cannot just pass kwargs around fastapi.
|
|
31
|
+
Probably for code highlighting or smth but the internal fastapi include_router
|
|
32
|
+
functions copy objects by explicity listing all the fields of the objects, so
|
|
33
|
+
our derived class cannot have custom fields and use the app or router functions.
|
|
34
|
+
|
|
35
|
+
Futhermore, the super-constructor removes or resets fields when it is called.
|
|
36
|
+
|
|
37
|
+
very frustrating, up yours fastapi
|
|
38
|
+
"""
|
|
39
|
+
# validate the route args contain an aws service entry for which we have an integration
|
|
40
|
+
# and an entry for 'aws_iam_arn' for permission to call that service.
|
|
41
|
+
selected_services = set(self._integration_registry).intersection(set(kwargs))
|
|
42
|
+
|
|
43
|
+
if not selected_services:
|
|
44
|
+
# no aws integration in this route, so just call super
|
|
45
|
+
return super().__init__(path, endpoint, **kwargs)
|
|
46
|
+
|
|
47
|
+
if len(selected_services) > 1:
|
|
48
|
+
raise ValueError(
|
|
49
|
+
f"Exactly one of {self._integration_registry.keys()} is required, but found {selected_services} in {kwargs.keys()}"
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
# pop the params from the kwargs to stop fastapi spazzing out
|
|
53
|
+
aws_service_name = selected_services.pop()
|
|
54
|
+
aws_service_value = kwargs.pop(aws_service_name)
|
|
55
|
+
aws_object_key = kwargs.pop("aws_object_key", None)
|
|
56
|
+
|
|
57
|
+
aws_iam_arn = kwargs.pop("aws_iam_arn", None)
|
|
58
|
+
if not aws_iam_arn:
|
|
59
|
+
raise ValueError("'aws_iam_arn' is required for AWS integrations.")
|
|
60
|
+
|
|
61
|
+
# extract the path parmaeters
|
|
62
|
+
if hasattr(self, "path_format"):
|
|
63
|
+
path_parameters = self._extract_path_parameters(self.path_format)
|
|
64
|
+
else:
|
|
65
|
+
path_parameters = []
|
|
66
|
+
|
|
67
|
+
# extract the mapping templates if present
|
|
68
|
+
aws_request_template = kwargs.pop("request_template", None)
|
|
69
|
+
aws_request_parameters = kwargs.pop("request_parameters", None)
|
|
70
|
+
aws_mapping_template = kwargs.pop("aws_mapping_template", None)
|
|
71
|
+
aws_response_template = kwargs.pop("response_template", None)
|
|
72
|
+
|
|
73
|
+
# this is hack because fastapi does explict member copies resulting in duplicate
|
|
74
|
+
# objects rather than copy-by-reference (which i expect).
|
|
75
|
+
self.openapi_extra = kwargs.pop("openapi_extra", {})
|
|
76
|
+
|
|
77
|
+
# APIRoute clears openapi_extra here if it is set, so preseve it before calling the super constructor
|
|
78
|
+
if "x-amazon-apigateway-integration" in self.openapi_extra:
|
|
79
|
+
integration = self.openapi_extra
|
|
80
|
+
else:
|
|
81
|
+
integration = None
|
|
82
|
+
|
|
83
|
+
super().__init__(path, endpoint, **kwargs)
|
|
84
|
+
|
|
85
|
+
# if we already have the x-int, it has been copied over the super constructor
|
|
86
|
+
# so do not build a new integration
|
|
87
|
+
if not integration:
|
|
88
|
+
# find the integration which matches one of our kwargs
|
|
89
|
+
integration_fn = self._integration_registry[aws_service_name]
|
|
90
|
+
|
|
91
|
+
integration_params = integration_fn(
|
|
92
|
+
aws_service_value,
|
|
93
|
+
iam_arn=aws_iam_arn,
|
|
94
|
+
path_parameters=path_parameters,
|
|
95
|
+
mapping_template=aws_mapping_template,
|
|
96
|
+
request_template=aws_request_template,
|
|
97
|
+
request_parameters=aws_request_parameters,
|
|
98
|
+
response_template=aws_response_template,
|
|
99
|
+
object_key=aws_object_key, # NB: s3 only
|
|
100
|
+
http_method="GET" if "GET" in self.methods else next(iter(self.methods)),
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
integration = self._create_integration(**integration_params)
|
|
104
|
+
|
|
105
|
+
if self.openapi_extra is None:
|
|
106
|
+
self.openapi_extra = {}
|
|
107
|
+
|
|
108
|
+
self.openapi_extra.update(integration)
|
|
109
|
+
|
|
110
|
+
@staticmethod
|
|
111
|
+
def _extract_path_parameters(path: str) -> List[str]:
|
|
112
|
+
formatter = Formatter()
|
|
113
|
+
return [fname for _, fname, _, _ in formatter.parse(path) if fname]
|
|
114
|
+
|
|
115
|
+
def _create_integration(
|
|
116
|
+
self,
|
|
117
|
+
uri: str,
|
|
118
|
+
integration_type: str,
|
|
119
|
+
credentials: str,
|
|
120
|
+
responses: dict = None,
|
|
121
|
+
request_template: dict = None,
|
|
122
|
+
request_parameters: dict = None,
|
|
123
|
+
http_method: str = "POST",
|
|
124
|
+
):
|
|
125
|
+
"""create the x-amazon-apigateway-integration block for the openapi spec
|
|
126
|
+
|
|
127
|
+
This block defines how a request is made to the backend function so is always a POST request
|
|
128
|
+
|
|
129
|
+
NB: uri is not required for mock integrations, so it should be optional
|
|
130
|
+
|
|
131
|
+
request_template must be a dict of mimetype->string
|
|
132
|
+
request_parameters map request (HTTP) to integration parameter
|
|
133
|
+
responses is a dict of integration response patterns (key) to output transform
|
|
134
|
+
|
|
135
|
+
see:
|
|
136
|
+
+ general: https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-swagger-extensions.html
|
|
137
|
+
+ request_templates: https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-swagger-extensions-integration-requestTemplates.html
|
|
138
|
+
+ request_parameters: https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-swagger-extensions-integration-requestParameters.html
|
|
139
|
+
+ responses: https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-swagger-extensions-integration-response.html
|
|
140
|
+
"""
|
|
141
|
+
assert integration_type in ("aws", "aws_proxy")
|
|
142
|
+
|
|
143
|
+
assert isinstance(responses, dict), "responses must be a dict [%s]" % str(
|
|
144
|
+
type(responses)
|
|
145
|
+
)
|
|
146
|
+
assert (
|
|
147
|
+
"default" in responses
|
|
148
|
+
), "expected 'default' in responses (recieved: '%s')" % str(
|
|
149
|
+
list(responses.keys())
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
integration = {
|
|
153
|
+
"uri": uri,
|
|
154
|
+
"httpMethod": http_method,
|
|
155
|
+
"type": integration_type,
|
|
156
|
+
"credentials": credentials,
|
|
157
|
+
"responses": responses,
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if request_template is not None:
|
|
161
|
+
# request templates must be a dict of mimetypes to strings
|
|
162
|
+
# https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-swagger-extensions-integration-requestTemplates.html
|
|
163
|
+
assert isinstance(
|
|
164
|
+
request_template, dict
|
|
165
|
+
), "request_template must be dict [%s]" % (str(type(request_template)))
|
|
166
|
+
|
|
167
|
+
request_template_mimetypes = ("application/json", "application/xml")
|
|
168
|
+
assert all(
|
|
169
|
+
x_ in request_template_mimetypes for x_ in request_template
|
|
170
|
+
), "only mimetypes are expected in request_templates (found: '%s')" % str(
|
|
171
|
+
list(request_template.keys())
|
|
172
|
+
)
|
|
173
|
+
assert all(
|
|
174
|
+
isinstance(x_, str) for x_ in request_template
|
|
175
|
+
), "all request templates must be strings (found: '%s')" % (
|
|
176
|
+
str([str(type(x)) for x in request_template.values()])
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
integration["requestTemplates"] = request_template
|
|
180
|
+
|
|
181
|
+
if request_parameters is not None:
|
|
182
|
+
# TODO: validate against: https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-swagger-extensions-integration-requestParameters.html
|
|
183
|
+
assert isinstance(
|
|
184
|
+
request_parameters, dict
|
|
185
|
+
), "request_parameters must be dict [%s]" % str(type(request_parameters))
|
|
186
|
+
integration["requestParameters"] = request_parameters
|
|
187
|
+
|
|
188
|
+
return {"x-amazon-apigateway-integration": integration}
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
from .integrations import *
|
fastapi_aws/router.py
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""AWSAPIRouter
|
|
2
|
+
|
|
3
|
+
inherited from APIRouter so we can pass kwargs to get, post, put, delete decorators
|
|
4
|
+
"""
|
|
5
|
+
from fastapi import APIRouter
|
|
6
|
+
from fastapi.types import DecoratedCallable
|
|
7
|
+
from typing import Any, Callable, Optional, Type
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
from .route import AWSAPIRoute
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class AWSAPIRouter(APIRouter):
|
|
14
|
+
def __init__(self, *args, route_class: Type[AWSAPIRoute] = AWSAPIRoute, **kwargs):
|
|
15
|
+
super().__init__(*args, route_class=route_class, **kwargs)
|
|
16
|
+
|
|
17
|
+
def add_api_route(
|
|
18
|
+
self,
|
|
19
|
+
path: str,
|
|
20
|
+
endpoint: Callable[..., Any],
|
|
21
|
+
**kwargs: Any
|
|
22
|
+
) -> None:
|
|
23
|
+
route_class = kwargs.pop("route_class_override", None) or self.route_class
|
|
24
|
+
route = route_class(path, endpoint, **kwargs)
|
|
25
|
+
self.routes.append(route)
|
|
26
|
+
|
|
27
|
+
def api_route(self, path: str, **kwargs: Any) -> Callable[[DecoratedCallable], DecoratedCallable]:
|
|
28
|
+
def decorator(func: DecoratedCallable) -> DecoratedCallable:
|
|
29
|
+
self.add_api_route(path, func, **kwargs)
|
|
30
|
+
return func
|
|
31
|
+
|
|
32
|
+
return decorator
|
|
33
|
+
|
|
34
|
+
# Override the HTTP method decorators to accept aws_lambda_uri
|
|
35
|
+
def get(self, path: str, **kwargs: Any):
|
|
36
|
+
return self.api_route(path, methods=["GET"], **kwargs)
|
|
37
|
+
|
|
38
|
+
def post(self, path: str, **kwargs: Any):
|
|
39
|
+
return self.api_route(path, methods=["POST"], **kwargs)
|
|
40
|
+
|
|
41
|
+
def put(self, path: str, **kwargs: Any):
|
|
42
|
+
return self.api_route(path, methods=["PUT"], **kwargs)
|
|
43
|
+
|
|
44
|
+
def delete(self, path: str, **kwargs: Any):
|
|
45
|
+
return self.api_route(path, methods=["DELETE"], **kwargs)
|