Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(event-handler): add appsync batch resolvers #1998

Merged
Merged
Show file tree
Hide file tree
Changes from 87 commits
Commits
Show all changes
107 commits
Select commit Hold shift + click to select a range
432093c
update changelog with latest changes
Mar 10, 2023
da1e745
Add batch processing to appsync handler
Mar 3, 2023
9c5cbd9
Extend router to accept List of events. Add functional test
Mar 3, 2023
4793efd
Add e2e tests
Mar 10, 2023
e3ca8c5
Add required package
Mar 10, 2023
d0fe867
Fix linter checks
Mar 10, 2023
bc45703
Refactor appsync resolver
Mar 23, 2023
b7a4391
Refactor code to use composition instead of inheritence
Mar 31, 2023
e1d2caa
Refactor appsync event handler
Apr 7, 2023
b1a49d6
merging from develop
leandrodamascena Apr 14, 2023
f201d25
Merge remote-tracking branch 'upstream/develop' into fix/1303-appsync…
leandrodamascena Apr 27, 2023
8fe6a8b
fix style
leandrodamascena Apr 27, 2023
252a796
Merge branch 'develop' into fix/1303-appsync-batch-invoke
leandrodamascena May 5, 2023
bb87fa3
Merge branch 'develop' into fix/1303-appsync-batch-invoke
heitorlessa Jun 8, 2023
2714b0f
Merge branch 'develop' into fix/1303-appsync-batch-invoke
leandrodamascena Jun 8, 2023
986b497
Merge branch 'develop' into fix/1303-appsync-batch-invoke
heitorlessa Jun 12, 2023
b21a0a1
Merge branch 'develop' into fix/1303-appsync-batch-invoke
leandrodamascena Jun 28, 2023
a96df34
Merge branch 'develop' into fix/1303-appsync-batch-invoke
mploski Jul 14, 2023
d5bbd09
Add support for async batch processing
Jul 21, 2023
9f8625e
Merge branch 'develop' into fix/1303-appsync-batch-invoke
leandrodamascena Aug 15, 2023
659a044
Fixing sonarcloud error
leandrodamascena Aug 15, 2023
13b0bcd
Adding docstring + increasing coverage
leandrodamascena Aug 15, 2023
db74f0a
Adding missing test + increasing coverage
leandrodamascena Aug 15, 2023
1acfe6e
Merge branch 'develop' into fix/1303-appsync-batch-invoke
leandrodamascena Aug 29, 2023
b19f4d3
Merge branch 'develop' into fix/1303-appsync-batch-invoke
leandrodamascena Sep 4, 2023
c0c45c5
Merge branch 'develop' into fix/1303-appsync-batch-invoke
leandrodamascena Sep 5, 2023
56ce1fd
Merge branch 'develop' into fix/1303-appsync-batch-invoke
leandrodamascena Sep 27, 2023
5ac0e3c
Start writing docs
Oct 31, 2023
f248769
Merge branch 'develop' into fix/1303-appsync-batch-invoke
leandrodamascena Dec 7, 2023
0d2f7e9
Merge branch 'develop' into fix/1303-appsync-batch-invoke
leandrodamascena Jan 23, 2024
5f44a2b
Refactoring examples
leandrodamascena Jan 23, 2024
ab7d5b9
Refactoring code + examples + documentation
leandrodamascena Jan 25, 2024
4711cdf
Merge branch 'develop' into fix/1303-appsync-batch-invoke
leandrodamascena Jan 25, 2024
50894b9
Moving e2e tests to the right folder
leandrodamascena Jan 25, 2024
3ae6f73
Moving e2e tests to the right folder
leandrodamascena Jan 25, 2024
733b888
Merge branch 'develop' into fix/1303-appsync-batch-invoke
leandrodamascena Jan 26, 2024
99b03c5
Adding partial failure
leandrodamascena Jan 26, 2024
32be46c
Adding partial failure
leandrodamascena Jan 26, 2024
4d07d3c
Fixing docstring and examples
leandrodamascena Jan 27, 2024
5644225
Adding documentation about Handling Exceptions
leandrodamascena Jan 27, 2024
78d330b
Merge branch 'develop' into fix/1303-appsync-batch-invoke
leandrodamascena Jan 29, 2024
13ac0a1
Merge branch 'develop' into fix/1303-appsync-batch-invoke
leandrodamascena Jan 29, 2024
b928996
Fixing docstring
leandrodamascena Jan 29, 2024
45c8a38
Merge branch 'develop' into fix/1303-appsync-batch-invoke
leandrodamascena Feb 1, 2024
48ee55f
Merge branch 'develop' into fix/1303-appsync-batch-invoke
leandrodamascena Feb 2, 2024
88d2151
Merging from develop
leandrodamascena Feb 6, 2024
f81a003
Adding fine grained control when handling exceptions
leandrodamascena Feb 7, 2024
25fff55
Adding fine grained control when handling exceptions
leandrodamascena Feb 7, 2024
5a335f6
Merge branch 'develop' into fix/1303-appsync-batch-invoke
heitorlessa Feb 7, 2024
cbe7190
docs: add intro diagram
heitorlessa Feb 9, 2024
38a77e8
Merge branch 'develop' into fix/1303-appsync-batch-invoke
heitorlessa Feb 21, 2024
f10a02c
docs: fix wording (Tech debt)
heitorlessa Feb 9, 2024
f0c7b36
refactor: use async_ prefix for async code
heitorlessa Feb 21, 2024
db6d67d
refactor: move router to a separate file to ease maintenance
heitorlessa Feb 21, 2024
50134fc
refactor: rename BasePublic to BaseRouter
heitorlessa Feb 21, 2024
14635f5
refactor: undo router context composition to reduce complexity and ca…
heitorlessa Feb 21, 2024
774a725
refactor: reduce abstractions, use explicit methods over assignments
heitorlessa Feb 21, 2024
e36dd7c
refactor: move registry to a separate file; make it private
heitorlessa Feb 21, 2024
3cddfff
refactor: expand inline if for readability
heitorlessa Feb 21, 2024
17015df
refactor: short circuit upfront, complex after
heitorlessa Feb 22, 2024
54d892b
refactor: simplify arg name
heitorlessa Feb 22, 2024
0ad3383
refactor: add debug statements
heitorlessa Feb 22, 2024
eb340ec
fix(docs): use .context instead of previous ._router.context
heitorlessa Feb 22, 2024
7326582
refactor: use kwargs for explicitness
heitorlessa Feb 22, 2024
26353fa
refactor: use return_exceptions=True to reduce call stack
heitorlessa Feb 23, 2024
b27d497
chore: add notes on the beauty of return_exceptions
heitorlessa Feb 23, 2024
2a1c785
refactor: append suffix in exceptions
heitorlessa Feb 23, 2024
98a8aff
chore: if over elif in short-circuit
heitorlessa Feb 23, 2024
32fd640
chore: improve logging; glad I learned this new f-string trick
heitorlessa Feb 23, 2024
de823a6
chore: fix debug statement location due to null resolvers
heitorlessa Feb 23, 2024
1698778
revert: debug graceful error flag due to non-determinism async
heitorlessa Feb 23, 2024
3e036f9
revert: debug stmt due to mypy; moving elsewhere
heitorlessa Feb 23, 2024
01251bc
docs: docstring resolver (tech debt)
heitorlessa Feb 23, 2024
d280742
docs: minimal batch_resolver docstring
heitorlessa Feb 23, 2024
507d854
chore: complete resolver docstring
heitorlessa Feb 23, 2024
f213510
Merge branch 'develop' into fix/1303-appsync-batch-invoke
heitorlessa Feb 23, 2024
a805ac3
Removing payload exception
Feb 27, 2024
be8b056
Merge branch 'develop' into fix/1303-appsync-batch-invoke
leandrodamascena Feb 27, 2024
8592194
Updating poetry
Feb 27, 2024
90e73d0
merging develop
Feb 27, 2024
69fd6ae
Merging from develop
leandrodamascena Mar 14, 2024
e14c73f
Merging from develop
leandrodamascena Mar 14, 2024
857375c
Merging from develop
leandrodamascena Jun 24, 2024
9ed003e
Addressing Heitor's feedback
leandrodamascena Jun 24, 2024
d039211
Merge branch 'develop' into fix/1303-appsync-batch-invoke
leandrodamascena Jun 24, 2024
84c3590
Refactoring to support aggregate events
leandrodamascena Jun 24, 2024
4493165
Refactoring examples + docs
leandrodamascena Jun 24, 2024
a778dcd
Addressing Heitor's feedback
leandrodamascena Jun 25, 2024
615c6d7
Merging from develop
leandrodamascena Jun 25, 2024
b0e1e3e
docs: add diagram to visualize n+1 problem
heitorlessa Jun 26, 2024
fb631ea
docs: improve wording in lambda invoke
heitorlessa Jun 26, 2024
1ebc157
docs: add diagram where n+1 problem shifts to Lambda runtime
heitorlessa Jun 26, 2024
de73d47
docs: add diagram where n+1 problem shifts to Lambda runtime w/ error…
heitorlessa Jun 26, 2024
4414d6b
docs: highlight lambda response for non-errors
heitorlessa Jun 26, 2024
3764819
docs(setup): increase table of contents depth to 5 to help redis and …
heitorlessa Jun 26, 2024
8606136
Adding examples
leandrodamascena Jun 26, 2024
4c84e52
docs: explain N+1 problem and organize content into sub-sections
heitorlessa Jun 26, 2024
06480d4
docs: clean up batch resolvers section; add typing
heitorlessa Jun 26, 2024
925040e
docs: clean up no-aggregate processing section
heitorlessa Jun 26, 2024
2428ebd
docs: clean up raise on error section
heitorlessa Jun 26, 2024
92db4a2
Adding examples
leandrodamascena Jun 26, 2024
8f3af70
docs: clean up async section
heitorlessa Jun 26, 2024
bc6cd12
docs: fix highlights, add missing code annotation
heitorlessa Jun 26, 2024
a4173e0
docs: rename snippets to match advanced section
heitorlessa Jun 26, 2024
ba173b3
Merge branch 'develop' into fix/1303-appsync-batch-invoke
heitorlessa Jun 26, 2024
5d3616a
Merging from develop
leandrodamascena Jun 26, 2024
a612e38
Tests
leandrodamascena Jun 26, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
440 changes: 343 additions & 97 deletions aws_lambda_powertools/event_handler/appsync.py

Large diffs are not rendered by default.

Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import logging
from typing import Any, Callable, Dict, Optional

logger = logging.getLogger(__name__)


class ResolverRegistry:
def __init__(self):
self.resolvers: Dict[str, Dict[str, Any]] = {}

def register(
self,
type_name: str = "*",
field_name: Optional[str] = None,
raise_on_error: bool = False,
aggregate: bool = True,
) -> Callable:
"""Registers the resolver for field_name

Parameters
----------
type_name : str
Type name
field_name : str
Field name
raise_on_error: bool
A flag indicating whether to raise an error when processing batches
with failed items. Defaults to False, which means errors are handled without raising exceptions.
aggregate: bool
A flag indicating whether the batch items should be processed at once or individually.
If True (default), the batch resolver will process all items in the batch as a single event.
If False, the batch resolver will process each item in the batch individually.

Return
----------
Dict
A dictionary with the resolver and if raise exception on error
"""

def _register(func) -> Callable:
logger.debug(f"Adding resolver `{func.__name__}` for field `{type_name}.{field_name}`")
self.resolvers[f"{type_name}.{field_name}"] = {
"func": func,
"raise_on_error": raise_on_error,
"aggregate": aggregate,
}
return func

return _register

def find_resolver(self, type_name: str, field_name: str) -> Optional[Dict]:
"""Find resolver based on type_name and field_name

Parameters
----------
type_name : str
Type name
field_name : str
Field name
Return
----------
Optional[Dict]
A dictionary with the resolver and if raise exception on error
"""
logger.debug(f"Looking for resolver for type={type_name}, field={field_name}.")
return self.resolvers.get(f"{type_name}.{field_name}", self.resolvers.get(f"*.{field_name}"))

def merge(self, other_registry: "ResolverRegistry"):
"""Update current registry with incoming registry

Parameters
----------
other_registry : ResolverRegistry
Registry to merge from
"""
self.resolvers.update(**other_registry.resolvers)
158 changes: 158 additions & 0 deletions aws_lambda_powertools/event_handler/graphql_appsync/base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
from abc import ABC, abstractmethod
from typing import Callable, Optional


class BaseRouter(ABC):
"""Abstract base class for Router (resolvers)"""

@abstractmethod
def resolver(self, type_name: str = "*", field_name: Optional[str] = None) -> Callable:
"""
Retrieve a resolver function for a specific type and field.

Parameters
-----------
type_name: str
The name of the type.
field_name: Optional[str]
The name of the field (default is None).

Examples
--------
```python
from typing import Optional

from aws_lambda_powertools.event_handler import AppSyncResolver
from aws_lambda_powertools.utilities.data_classes import AppSyncResolverEvent
from aws_lambda_powertools.utilities.typing import LambdaContext

app = AppSyncResolver()

@app.resolver(type_name="Query", field_name="getPost")
def related_posts(event: AppSyncResolverEvent) -> Optional[list]:
return {"success": "ok"}

def lambda_handler(event, context: LambdaContext) -> dict:
return app.resolve(event, context)
```

Returns
-------
Callable
The resolver function.
"""
raise NotImplementedError

@abstractmethod
def batch_resolver(
self,
type_name: str = "*",
field_name: Optional[str] = None,
raise_on_error: bool = False,
aggregate: bool = True,
) -> Callable:
"""
Retrieve a batch resolver function for a specific type and field.

Parameters
-----------
type_name: str
The name of the type.
field_name: Optional[str]
The name of the field (default is None).
raise_on_error: bool
A flag indicating whether to raise an error when processing batches
with failed items. Defaults to False, which means errors are handled without raising exceptions.
aggregate: bool
A flag indicating whether the batch items should be processed at once or individually.
If True (default), the batch resolver will process all items in the batch as a single event.
If False, the batch resolver will process each item in the batch individually.

Examples
--------
```python
from typing import Optional

from aws_lambda_powertools.event_handler import AppSyncResolver
from aws_lambda_powertools.utilities.data_classes import AppSyncResolverEvent
from aws_lambda_powertools.utilities.typing import LambdaContext

app = AppSyncResolver()

@app.batch_resolver(type_name="Query", field_name="getPost")
def related_posts(event: AppSyncResolverEvent, id) -> Optional[list]:
return {"post_id": id}

def lambda_handler(event, context: LambdaContext) -> dict:
return app.resolve(event, context)
```

Returns
-------
Callable
The batch resolver function.
"""
raise NotImplementedError

@abstractmethod
def async_batch_resolver(
self,
type_name: str = "*",
field_name: Optional[str] = None,
raise_on_error: bool = False,
aggregate: bool = True,
) -> Callable:
"""
Retrieve a batch resolver function for a specific type and field and runs async.

Parameters
-----------
type_name: str
The name of the type.
field_name: Optional[str]
The name of the field (default is None).
raise_on_error: bool
A flag indicating whether to raise an error when processing batches
with failed items. Defaults to False, which means errors are handled without raising exceptions.
aggregate: bool
A flag indicating whether the batch items should be processed at once or individually.
If True (default), the batch resolver will process all items in the batch as a single event.
If False, the batch resolver will process each item in the batch individually.

Examples
--------
```python
from typing import Optional

from aws_lambda_powertools.event_handler import AppSyncResolver
from aws_lambda_powertools.utilities.data_classes import AppSyncResolverEvent
from aws_lambda_powertools.utilities.typing import LambdaContext

app = AppSyncResolver()

@app.async_batch_resolver(type_name="Query", field_name="getPost")
async def related_posts(event: AppSyncResolverEvent, id) -> Optional[list]:
return {"post_id": id}

def lambda_handler(event, context: LambdaContext) -> dict:
return app.resolve(event, context)
```

Returns
-------
Callable
The batch resolver function.
"""
raise NotImplementedError

@abstractmethod
def append_context(self, **additional_context) -> None:
"""
Appends context information available under any route.

Parameters
-----------
**additional_context: dict
Additional context key-value pairs to append.
"""
raise NotImplementedError
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
class ResolverNotFoundError(Exception):
"""
When a resolver is not found during a lookup.
"""


class InvalidBatchResponse(Exception):
"""
When a batch response something different from a List
"""
53 changes: 53 additions & 0 deletions aws_lambda_powertools/event_handler/graphql_appsync/router.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
from typing import Callable, Optional

from aws_lambda_powertools.event_handler.graphql_appsync._registry import ResolverRegistry
from aws_lambda_powertools.event_handler.graphql_appsync.base import BaseRouter


class Router(BaseRouter):
context: dict

def __init__(self):
self.context = {} # early init as customers might add context before event resolution
self._resolver_registry = ResolverRegistry()
self._batch_resolver_registry = ResolverRegistry()
self._async_batch_resolver_registry = ResolverRegistry()

def resolver(self, type_name: str = "*", field_name: Optional[str] = None) -> Callable:
return self._resolver_registry.register(field_name=field_name, type_name=type_name)

def batch_resolver(
self,
type_name: str = "*",
field_name: Optional[str] = None,
raise_on_error: bool = False,
aggregate: bool = True,
) -> Callable:
return self._batch_resolver_registry.register(
field_name=field_name,
type_name=type_name,
raise_on_error=raise_on_error,
aggregate=aggregate,
)

def async_batch_resolver(
self,
type_name: str = "*",
field_name: Optional[str] = None,
raise_on_error: bool = False,
aggregate: bool = True,
) -> Callable:
return self._async_batch_resolver_registry.register(
field_name=field_name,
type_name=type_name,
raise_on_error=raise_on_error,
aggregate=aggregate,
)

def append_context(self, **additional_context):
"""Append key=value data as routing context"""
self.context.update(**additional_context)

def clear_context(self):
"""Resets routing context"""
self.context.clear()
2 changes: 1 addition & 1 deletion aws_lambda_powertools/utilities/parameters/secrets.py
Original file line number Diff line number Diff line change
Expand Up @@ -423,7 +423,7 @@ def set_secret(
>>> parameters.set_secret(
name="my-secret",
value='{"password": "supers3cr3tllam@passw0rd"}',
client_request_token="61f2af5f-5f75-44b1-a29f-0cc37af55b11"
client_request_token="YOUR_TOKEN_HERE"
)

URLs:
Expand Down
Loading