aws-lambda-powertools 3.0.1.dev3__py3-none-any.whl → 3.0.2.dev1__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.
- aws_lambda_powertools/event_handler/api_gateway.py +227 -54
- aws_lambda_powertools/event_handler/appsync.py +343 -97
- aws_lambda_powertools/event_handler/bedrock_agent.py +121 -0
- aws_lambda_powertools/event_handler/exceptions.py +1 -1
- aws_lambda_powertools/event_handler/graphql_appsync/__init__.py +0 -0
- aws_lambda_powertools/event_handler/graphql_appsync/_registry.py +78 -0
- aws_lambda_powertools/event_handler/graphql_appsync/base.py +160 -0
- aws_lambda_powertools/event_handler/graphql_appsync/exceptions.py +10 -0
- aws_lambda_powertools/event_handler/graphql_appsync/router.py +62 -0
- aws_lambda_powertools/event_handler/middlewares/openapi_validation.py +1 -1
- aws_lambda_powertools/event_handler/openapi/encoders.py +94 -70
- aws_lambda_powertools/event_handler/openapi/exceptions.py +12 -0
- aws_lambda_powertools/event_handler/openapi/models.py +38 -5
- aws_lambda_powertools/event_handler/openapi/swagger_ui/html.py +1 -4
- aws_lambda_powertools/event_handler/util.py +55 -2
- aws_lambda_powertools/logging/utils.py +5 -5
- aws_lambda_powertools/metrics/provider/cloudwatch_emf/metric_properties.py +1 -0
- aws_lambda_powertools/shared/cookies.py +2 -2
- aws_lambda_powertools/shared/dynamodb_deserializer.py +13 -0
- aws_lambda_powertools/shared/version.py +1 -1
- aws_lambda_powertools/tracing/tracer.py +4 -2
- aws_lambda_powertools/utilities/batch/base.py +13 -4
- aws_lambda_powertools/utilities/data_classes/cognito_user_pool_event.py +260 -1
- aws_lambda_powertools/utilities/data_classes/kafka_event.py +2 -2
- aws_lambda_powertools/utilities/data_classes/vpc_lattice.py +7 -0
- aws_lambda_powertools/utilities/data_masking/base.py +4 -4
- aws_lambda_powertools/utilities/feature_flags/appconfig.py +21 -2
- aws_lambda_powertools/utilities/idempotency/idempotency.py +22 -2
- aws_lambda_powertools/utilities/idempotency/persistence/base.py +4 -1
- aws_lambda_powertools/utilities/parameters/secrets.py +1 -1
- aws_lambda_powertools/utilities/parser/models/__init__.py +6 -0
- aws_lambda_powertools/utilities/parser/models/apigw.py +12 -1
- aws_lambda_powertools/utilities/parser/models/apigwv2.py +7 -1
- aws_lambda_powertools/utilities/validation/base.py +27 -2
- aws_lambda_powertools/utilities/validation/validator.py +49 -4
- {aws_lambda_powertools-3.0.1.dev3.dist-info → aws_lambda_powertools-3.0.2.dev1.dist-info}/METADATA +12 -2
- {aws_lambda_powertools-3.0.1.dev3.dist-info → aws_lambda_powertools-3.0.2.dev1.dist-info}/RECORD +39 -34
- {aws_lambda_powertools-3.0.1.dev3.dist-info → aws_lambda_powertools-3.0.2.dev1.dist-info}/LICENSE +0 -0
- {aws_lambda_powertools-3.0.1.dev3.dist-info → aws_lambda_powertools-3.0.2.dev1.dist-info}/WHEEL +0 -0
|
@@ -1,100 +1,80 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import asyncio
|
|
3
4
|
import logging
|
|
4
|
-
|
|
5
|
+
import warnings
|
|
6
|
+
from typing import TYPE_CHECKING, Any, Callable
|
|
5
7
|
|
|
8
|
+
from aws_lambda_powertools.event_handler.graphql_appsync.exceptions import InvalidBatchResponse, ResolverNotFoundError
|
|
9
|
+
from aws_lambda_powertools.event_handler.graphql_appsync.router import Router
|
|
6
10
|
from aws_lambda_powertools.utilities.data_classes import AppSyncResolverEvent
|
|
7
11
|
|
|
8
12
|
if TYPE_CHECKING:
|
|
9
13
|
from aws_lambda_powertools.utilities.typing import LambdaContext
|
|
10
14
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
AppSyncResolverEventT = TypeVar("AppSyncResolverEventT", bound=AppSyncResolverEvent)
|
|
14
|
-
|
|
15
|
+
from aws_lambda_powertools.warnings import PowertoolsUserWarning
|
|
15
16
|
|
|
16
|
-
|
|
17
|
-
current_event: AppSyncResolverEventT # type: ignore[valid-type]
|
|
18
|
-
lambda_context: LambdaContext
|
|
19
|
-
context: dict
|
|
20
|
-
|
|
21
|
-
def __init__(self):
|
|
22
|
-
self._resolvers: dict = {}
|
|
23
|
-
|
|
24
|
-
def resolver(self, type_name: str = "*", field_name: str | None = None):
|
|
25
|
-
"""Registers the resolver for field_name
|
|
26
|
-
|
|
27
|
-
Parameters
|
|
28
|
-
----------
|
|
29
|
-
type_name : str
|
|
30
|
-
Type name
|
|
31
|
-
field_name : str
|
|
32
|
-
Field name
|
|
33
|
-
"""
|
|
34
|
-
|
|
35
|
-
def register_resolver(func):
|
|
36
|
-
logger.debug(f"Adding resolver `{func.__name__}` for field `{type_name}.{field_name}`")
|
|
37
|
-
self._resolvers[f"{type_name}.{field_name}"] = {"func": func}
|
|
38
|
-
return func
|
|
39
|
-
|
|
40
|
-
return register_resolver
|
|
41
|
-
|
|
42
|
-
def append_context(self, **additional_context):
|
|
43
|
-
"""Append key=value data as routing context"""
|
|
44
|
-
self.context.update(**additional_context)
|
|
45
|
-
|
|
46
|
-
def clear_context(self):
|
|
47
|
-
"""Resets routing context"""
|
|
48
|
-
self.context.clear()
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
49
18
|
|
|
50
19
|
|
|
51
|
-
class AppSyncResolver(
|
|
20
|
+
class AppSyncResolver(Router):
|
|
52
21
|
"""
|
|
53
|
-
AppSync
|
|
22
|
+
AppSync GraphQL API Resolver
|
|
54
23
|
|
|
55
24
|
Example
|
|
56
25
|
-------
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
return str(uuid.uuid4())
|
|
26
|
+
```python
|
|
27
|
+
from aws_lambda_powertools.event_handler import AppSyncResolver
|
|
28
|
+
|
|
29
|
+
app = AppSyncResolver()
|
|
30
|
+
|
|
31
|
+
@app.resolver(type_name="Query", field_name="listLocations")
|
|
32
|
+
def list_locations(page: int = 0, size: int = 10) -> list:
|
|
33
|
+
# Your logic to fetch locations with arguments passed in
|
|
34
|
+
return [{"id": 100, "name": "Smooth Grooves"}]
|
|
35
|
+
|
|
36
|
+
@app.resolver(type_name="Merchant", field_name="extraInfo")
|
|
37
|
+
def get_extra_info() -> dict:
|
|
38
|
+
# Can use "app.current_event.source" to filter within the parent context
|
|
39
|
+
account_type = app.current_event.source["accountType"]
|
|
40
|
+
method = "BTC" if account_type == "NEW" else "USD"
|
|
41
|
+
return {"preferredPaymentMethod": method}
|
|
42
|
+
|
|
43
|
+
@app.resolver(field_name="commonField")
|
|
44
|
+
def common_field() -> str:
|
|
45
|
+
# Would match all fieldNames matching 'commonField'
|
|
46
|
+
return str(uuid.uuid4())
|
|
47
|
+
```
|
|
80
48
|
"""
|
|
81
49
|
|
|
82
50
|
def __init__(self):
|
|
51
|
+
"""
|
|
52
|
+
Initialize a new instance of the AppSyncResolver.
|
|
53
|
+
"""
|
|
83
54
|
super().__init__()
|
|
84
55
|
self.context = {} # early init as customers might add context before event resolution
|
|
85
56
|
|
|
86
|
-
def
|
|
57
|
+
def __call__(
|
|
87
58
|
self,
|
|
88
59
|
event: dict,
|
|
89
60
|
context: LambdaContext,
|
|
90
61
|
data_model: type[AppSyncResolverEvent] = AppSyncResolverEvent,
|
|
91
62
|
) -> Any:
|
|
92
|
-
"""
|
|
63
|
+
"""Implicit lambda handler which internally calls `resolve`"""
|
|
64
|
+
return self.resolve(event, context, data_model)
|
|
65
|
+
|
|
66
|
+
def resolve(
|
|
67
|
+
self,
|
|
68
|
+
event: dict | list[dict],
|
|
69
|
+
context: LambdaContext,
|
|
70
|
+
data_model: type[AppSyncResolverEvent] = AppSyncResolverEvent,
|
|
71
|
+
) -> Any:
|
|
72
|
+
"""Resolves the response based on the provide event and decorator routes
|
|
93
73
|
|
|
94
74
|
Parameters
|
|
95
75
|
----------
|
|
96
|
-
event : dict
|
|
97
|
-
Lambda event
|
|
76
|
+
event : dict | list[Dict]
|
|
77
|
+
Lambda event either coming from batch processing endpoint or from standard processing endpoint
|
|
98
78
|
context : LambdaContext
|
|
99
79
|
Lambda context
|
|
100
80
|
data_model:
|
|
@@ -158,45 +138,214 @@ class AppSyncResolver(BaseRouter):
|
|
|
158
138
|
ValueError
|
|
159
139
|
If we could not find a field resolver
|
|
160
140
|
"""
|
|
161
|
-
# Maintenance: revisit generics/overload to fix [attr-defined] in mypy usage
|
|
162
|
-
BaseRouter.current_event = data_model(event)
|
|
163
|
-
BaseRouter.lambda_context = context
|
|
164
141
|
|
|
165
|
-
|
|
166
|
-
|
|
142
|
+
self.lambda_context = context
|
|
143
|
+
Router.lambda_context = context
|
|
144
|
+
|
|
145
|
+
if isinstance(event, list):
|
|
146
|
+
Router.current_batch_event = [data_model(e) for e in event]
|
|
147
|
+
response = self._call_batch_resolver(event=event, data_model=data_model)
|
|
148
|
+
else:
|
|
149
|
+
Router.current_event = data_model(event)
|
|
150
|
+
response = self._call_single_resolver(event=event, data_model=data_model)
|
|
151
|
+
|
|
167
152
|
self.clear_context()
|
|
168
153
|
|
|
169
154
|
return response
|
|
170
155
|
|
|
171
|
-
def
|
|
172
|
-
"""
|
|
156
|
+
def _call_single_resolver(self, event: dict, data_model: type[AppSyncResolverEvent]) -> Any:
|
|
157
|
+
"""Call single event resolver
|
|
173
158
|
|
|
174
159
|
Parameters
|
|
175
160
|
----------
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
161
|
+
event : dict
|
|
162
|
+
Event
|
|
163
|
+
data_model : type[AppSyncResolverEvent]
|
|
164
|
+
Data_model to decode AppSync event, by default it is of AppSyncResolverEvent type or subclass of it
|
|
165
|
+
"""
|
|
166
|
+
|
|
167
|
+
logger.debug("Processing direct resolver event")
|
|
168
|
+
|
|
169
|
+
self.current_event = data_model(event)
|
|
170
|
+
resolver = self._resolver_registry.find_resolver(self.current_event.type_name, self.current_event.field_name)
|
|
171
|
+
if not resolver:
|
|
172
|
+
raise ValueError(f"No resolver found for '{self.current_event.type_name}.{self.current_event.field_name}'")
|
|
173
|
+
return resolver["func"](**self.current_event.arguments)
|
|
174
|
+
|
|
175
|
+
def _call_sync_batch_resolver(
|
|
176
|
+
self,
|
|
177
|
+
resolver: Callable,
|
|
178
|
+
raise_on_error: bool = False,
|
|
179
|
+
aggregate: bool = True,
|
|
180
|
+
) -> list[Any]:
|
|
181
|
+
"""
|
|
182
|
+
Calls a synchronous batch resolver function for each event in the current batch.
|
|
183
|
+
|
|
184
|
+
Parameters
|
|
185
|
+
----------
|
|
186
|
+
resolver: Callable
|
|
187
|
+
The callable function to resolve events.
|
|
188
|
+
raise_on_error: bool
|
|
189
|
+
A flag indicating whether to raise an error when processing batches
|
|
190
|
+
with failed items. Defaults to False, which means errors are handled without raising exceptions.
|
|
191
|
+
aggregate: bool
|
|
192
|
+
A flag indicating whether the batch items should be processed at once or individually.
|
|
193
|
+
If True (default), the batch resolver will process all items in the batch as a single event.
|
|
194
|
+
If False, the batch resolver will process each item in the batch individually.
|
|
180
195
|
|
|
181
196
|
Returns
|
|
182
197
|
-------
|
|
183
|
-
|
|
184
|
-
|
|
198
|
+
list[Any]
|
|
199
|
+
A list of results corresponding to the resolved events.
|
|
185
200
|
"""
|
|
186
|
-
full_name = f"{type_name}.{field_name}"
|
|
187
|
-
resolver = self._resolvers.get(full_name, self._resolvers.get(f"*.{field_name}"))
|
|
188
|
-
if not resolver:
|
|
189
|
-
raise ValueError(f"No resolver found for '{full_name}'")
|
|
190
|
-
return resolver["func"]
|
|
191
201
|
|
|
192
|
-
|
|
202
|
+
logger.debug(f"Graceful error handling flag {raise_on_error=}")
|
|
203
|
+
|
|
204
|
+
# Checks whether the entire batch should be processed at once
|
|
205
|
+
if aggregate:
|
|
206
|
+
# Process the entire batch
|
|
207
|
+
response = resolver(event=self.current_batch_event)
|
|
208
|
+
|
|
209
|
+
if not isinstance(response, list):
|
|
210
|
+
raise InvalidBatchResponse("The response must be a List when using batch resolvers")
|
|
211
|
+
|
|
212
|
+
return response
|
|
213
|
+
|
|
214
|
+
# Non aggregated events, so we call this event list x times
|
|
215
|
+
# Stop on first exception we encounter
|
|
216
|
+
if raise_on_error:
|
|
217
|
+
return [
|
|
218
|
+
resolver(event=appconfig_event, **appconfig_event.arguments)
|
|
219
|
+
for appconfig_event in self.current_batch_event
|
|
220
|
+
]
|
|
221
|
+
|
|
222
|
+
# By default, we gracefully append `None` for any records that failed processing
|
|
223
|
+
results = []
|
|
224
|
+
for idx, event in enumerate(self.current_batch_event):
|
|
225
|
+
try:
|
|
226
|
+
results.append(resolver(event=event, **event.arguments))
|
|
227
|
+
except Exception:
|
|
228
|
+
logger.debug(f"Failed to process event number {idx} from field '{event.info.field_name}'")
|
|
229
|
+
results.append(None)
|
|
230
|
+
|
|
231
|
+
return results
|
|
232
|
+
|
|
233
|
+
async def _call_async_batch_resolver(
|
|
193
234
|
self,
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
) -> Any:
|
|
198
|
-
"""
|
|
199
|
-
|
|
235
|
+
resolver: Callable,
|
|
236
|
+
raise_on_error: bool = False,
|
|
237
|
+
aggregate: bool = True,
|
|
238
|
+
) -> list[Any]:
|
|
239
|
+
"""
|
|
240
|
+
Asynchronously call a batch resolver for each event in the current batch.
|
|
241
|
+
|
|
242
|
+
Parameters
|
|
243
|
+
----------
|
|
244
|
+
resolver: Callable
|
|
245
|
+
The asynchronous resolver function.
|
|
246
|
+
raise_on_error: bool
|
|
247
|
+
A flag indicating whether to raise an error when processing batches
|
|
248
|
+
with failed items. Defaults to False, which means errors are handled without raising exceptions.
|
|
249
|
+
aggregate: bool
|
|
250
|
+
A flag indicating whether the batch items should be processed at once or individually.
|
|
251
|
+
If True (default), the batch resolver will process all items in the batch as a single event.
|
|
252
|
+
If False, the batch resolver will process each item in the batch individually.
|
|
253
|
+
|
|
254
|
+
Returns
|
|
255
|
+
-------
|
|
256
|
+
list[Any]
|
|
257
|
+
A list of results corresponding to the resolved events.
|
|
258
|
+
"""
|
|
259
|
+
|
|
260
|
+
logger.debug(f"Graceful error handling flag {raise_on_error=}")
|
|
261
|
+
|
|
262
|
+
# Checks whether the entire batch should be processed at once
|
|
263
|
+
if aggregate:
|
|
264
|
+
# Process the entire batch
|
|
265
|
+
ret = await resolver(event=self.current_batch_event)
|
|
266
|
+
if not isinstance(ret, list):
|
|
267
|
+
raise InvalidBatchResponse("The response must be a List when using batch resolvers")
|
|
268
|
+
|
|
269
|
+
return ret
|
|
270
|
+
|
|
271
|
+
response: list = []
|
|
272
|
+
|
|
273
|
+
# Prime coroutines
|
|
274
|
+
tasks = [resolver(event=e, **e.arguments) for e in self.current_batch_event]
|
|
275
|
+
|
|
276
|
+
# Aggregate results or raise at first error
|
|
277
|
+
if raise_on_error:
|
|
278
|
+
response.extend(await asyncio.gather(*tasks))
|
|
279
|
+
return response
|
|
280
|
+
|
|
281
|
+
# Aggregate results and exceptions, then filter them out
|
|
282
|
+
# Use `None` upon exception for graceful error handling at GraphQL engine level
|
|
283
|
+
#
|
|
284
|
+
# NOTE: asyncio.gather(return_exceptions=True) catches and includes exceptions in the results
|
|
285
|
+
# this will become useful when we support exception handling in AppSync resolver
|
|
286
|
+
results = await asyncio.gather(*tasks, return_exceptions=True)
|
|
287
|
+
response.extend(None if isinstance(ret, Exception) else ret for ret in results)
|
|
288
|
+
|
|
289
|
+
return response
|
|
290
|
+
|
|
291
|
+
def _call_batch_resolver(self, event: list[dict], data_model: type[AppSyncResolverEvent]) -> list[Any]:
|
|
292
|
+
"""Call batch event resolver for sync and async methods
|
|
293
|
+
|
|
294
|
+
Parameters
|
|
295
|
+
----------
|
|
296
|
+
event : list[dict]
|
|
297
|
+
Batch event
|
|
298
|
+
data_model : type[AppSyncResolverEvent]
|
|
299
|
+
Data_model to decode AppSync event, by default AppSyncResolverEvent or a subclass
|
|
300
|
+
|
|
301
|
+
Returns
|
|
302
|
+
-------
|
|
303
|
+
list[Any]
|
|
304
|
+
Results of the resolver execution.
|
|
305
|
+
|
|
306
|
+
Raises
|
|
307
|
+
------
|
|
308
|
+
InconsistentPayloadError:
|
|
309
|
+
When all events in the batch do not have the same fieldName.
|
|
310
|
+
|
|
311
|
+
ResolverNotFoundError:
|
|
312
|
+
When no resolver is found for the specified type and field.
|
|
313
|
+
"""
|
|
314
|
+
logger.debug("Processing batch resolver event")
|
|
315
|
+
|
|
316
|
+
self.current_batch_event = [data_model(e) for e in event]
|
|
317
|
+
type_name, field_name = self.current_batch_event[0].type_name, self.current_batch_event[0].field_name
|
|
318
|
+
|
|
319
|
+
resolver = self._batch_resolver_registry.find_resolver(type_name, field_name)
|
|
320
|
+
async_resolver = self._async_batch_resolver_registry.find_resolver(type_name, field_name)
|
|
321
|
+
|
|
322
|
+
if resolver and async_resolver:
|
|
323
|
+
warnings.warn(
|
|
324
|
+
f"Both synchronous and asynchronous resolvers found for the same event and field."
|
|
325
|
+
f"The synchronous resolver takes precedence. Executing: {resolver['func'].__name__}",
|
|
326
|
+
stacklevel=2,
|
|
327
|
+
category=PowertoolsUserWarning,
|
|
328
|
+
)
|
|
329
|
+
|
|
330
|
+
if resolver:
|
|
331
|
+
logger.debug(f"Found sync resolver. {resolver=}, {field_name=}")
|
|
332
|
+
return self._call_sync_batch_resolver(
|
|
333
|
+
resolver=resolver["func"],
|
|
334
|
+
raise_on_error=resolver["raise_on_error"],
|
|
335
|
+
aggregate=resolver["aggregate"],
|
|
336
|
+
)
|
|
337
|
+
|
|
338
|
+
if async_resolver:
|
|
339
|
+
logger.debug(f"Found async resolver. {resolver=}, {field_name=}")
|
|
340
|
+
return asyncio.run(
|
|
341
|
+
self._call_async_batch_resolver(
|
|
342
|
+
resolver=async_resolver["func"],
|
|
343
|
+
raise_on_error=async_resolver["raise_on_error"],
|
|
344
|
+
aggregate=async_resolver["aggregate"],
|
|
345
|
+
),
|
|
346
|
+
)
|
|
347
|
+
|
|
348
|
+
raise ResolverNotFoundError(f"No resolver found for '{type_name}.{field_name}'")
|
|
200
349
|
|
|
201
350
|
def include_router(self, router: Router) -> None:
|
|
202
351
|
"""Adds all resolvers defined in a router
|
|
@@ -206,15 +355,112 @@ class AppSyncResolver(BaseRouter):
|
|
|
206
355
|
router : Router
|
|
207
356
|
A router containing a dict of field resolvers
|
|
208
357
|
"""
|
|
358
|
+
|
|
209
359
|
# Merge app and router context
|
|
360
|
+
logger.debug("Merging router and app context")
|
|
210
361
|
self.context.update(**router.context)
|
|
362
|
+
|
|
211
363
|
# use pointer to allow context clearance after event is processed e.g., resolve(evt, ctx)
|
|
212
364
|
router.context = self.context
|
|
213
365
|
|
|
214
|
-
|
|
366
|
+
logger.debug("Merging router resolver registries")
|
|
367
|
+
self._resolver_registry.merge(router._resolver_registry)
|
|
368
|
+
self._batch_resolver_registry.merge(router._batch_resolver_registry)
|
|
369
|
+
self._async_batch_resolver_registry.merge(router._async_batch_resolver_registry)
|
|
215
370
|
|
|
371
|
+
def resolver(self, type_name: str = "*", field_name: str | None = None) -> Callable:
|
|
372
|
+
"""Registers direct resolver function for GraphQL type and field name.
|
|
216
373
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
374
|
+
Parameters
|
|
375
|
+
----------
|
|
376
|
+
type_name : str, optional
|
|
377
|
+
GraphQL type e.g., Query, Mutation, by default "*" meaning any
|
|
378
|
+
field_name : Optional[str], optional
|
|
379
|
+
GraphQL field e.g., getTodo, createTodo, by default None
|
|
380
|
+
|
|
381
|
+
Returns
|
|
382
|
+
-------
|
|
383
|
+
Callable
|
|
384
|
+
Registered resolver
|
|
385
|
+
|
|
386
|
+
Example
|
|
387
|
+
-------
|
|
388
|
+
|
|
389
|
+
```python
|
|
390
|
+
from aws_lambda_powertools.event_handler import AppSyncResolver
|
|
391
|
+
|
|
392
|
+
from typing import TypedDict
|
|
393
|
+
|
|
394
|
+
app = AppSyncResolver()
|
|
395
|
+
|
|
396
|
+
class Todo(TypedDict, total=False):
|
|
397
|
+
id: str
|
|
398
|
+
userId: str
|
|
399
|
+
title: str
|
|
400
|
+
completed: bool
|
|
401
|
+
|
|
402
|
+
# resolve any GraphQL `getTodo` queries
|
|
403
|
+
# arguments are injected as function arguments as-is
|
|
404
|
+
@app.resolver(type_name="Query", field_name="getTodo")
|
|
405
|
+
def get_todo(id: str = "", status: str = "open") -> Todo:
|
|
406
|
+
todos: Response = requests.get(f"https://jsonplaceholder.typicode.com/todos/{id}")
|
|
407
|
+
todos.raise_for_status()
|
|
408
|
+
|
|
409
|
+
return todos.json()
|
|
410
|
+
|
|
411
|
+
def lambda_handler(event, context):
|
|
412
|
+
return app.resolve(event, context)
|
|
413
|
+
```
|
|
414
|
+
"""
|
|
415
|
+
return self._resolver_registry.register(field_name=field_name, type_name=type_name)
|
|
416
|
+
|
|
417
|
+
def batch_resolver(
|
|
418
|
+
self,
|
|
419
|
+
type_name: str = "*",
|
|
420
|
+
field_name: str | None = None,
|
|
421
|
+
raise_on_error: bool = False,
|
|
422
|
+
aggregate: bool = True,
|
|
423
|
+
) -> Callable:
|
|
424
|
+
"""Registers batch resolver function for GraphQL type and field name.
|
|
425
|
+
|
|
426
|
+
By default, we handle errors gracefully by returning `None`. If you want
|
|
427
|
+
to short-circuit and fail the entire batch use `raise_on_error=True`.
|
|
428
|
+
|
|
429
|
+
Parameters
|
|
430
|
+
----------
|
|
431
|
+
type_name : str, optional
|
|
432
|
+
GraphQL type e.g., Query, Mutation, by default "*" meaning any
|
|
433
|
+
field_name : Optional[str], optional
|
|
434
|
+
GraphQL field e.g., getTodo, createTodo, by default None
|
|
435
|
+
raise_on_error : bool, optional
|
|
436
|
+
Whether to fail entire batch upon error, or handle errors gracefully (None), by default False
|
|
437
|
+
aggregate: bool
|
|
438
|
+
A flag indicating whether the batch items should be processed at once or individually.
|
|
439
|
+
If True (default), the batch resolver will process all items in the batch as a single event.
|
|
440
|
+
If False, the batch resolver will process each item in the batch individually.
|
|
441
|
+
|
|
442
|
+
Returns
|
|
443
|
+
-------
|
|
444
|
+
Callable
|
|
445
|
+
Registered resolver
|
|
446
|
+
"""
|
|
447
|
+
return self._batch_resolver_registry.register(
|
|
448
|
+
field_name=field_name,
|
|
449
|
+
type_name=type_name,
|
|
450
|
+
raise_on_error=raise_on_error,
|
|
451
|
+
aggregate=aggregate,
|
|
452
|
+
)
|
|
453
|
+
|
|
454
|
+
def async_batch_resolver(
|
|
455
|
+
self,
|
|
456
|
+
type_name: str = "*",
|
|
457
|
+
field_name: str | None = None,
|
|
458
|
+
raise_on_error: bool = False,
|
|
459
|
+
aggregate: bool = True,
|
|
460
|
+
) -> Callable:
|
|
461
|
+
return self._async_batch_resolver_registry.register(
|
|
462
|
+
field_name=field_name,
|
|
463
|
+
type_name=type_name,
|
|
464
|
+
raise_on_error=raise_on_error,
|
|
465
|
+
aggregate=aggregate,
|
|
466
|
+
)
|