` for version bump
diff --git a/README.md b/README.md
index 333a6e165ef..be449899676 100644
--- a/README.md
+++ b/README.md
@@ -53,6 +53,7 @@ Knowing which companies are using this library is important to help prioritize t
The following companies, among others, use Powertools:
+* [Brsk](https://www.brsk.co.uk/)
* [Capital One](https://www.capitalone.com/)
* [CPQi (Exadel Financial Services)](https://cpqi.com/)
* [CloudZero](https://www.cloudzero.com/)
diff --git a/aws_lambda_powertools/event_handler/appsync.py b/aws_lambda_powertools/event_handler/appsync.py
index fba5681ef6a..fb53b71c77d 100644
--- a/aws_lambda_powertools/event_handler/appsync.py
+++ b/aws_lambda_powertools/event_handler/appsync.py
@@ -1,96 +1,79 @@
+import asyncio
import logging
-from typing import Any, Callable, Optional, Type, TypeVar
+import warnings
+from typing import Any, Callable, Dict, List, Optional, Type, Union
+from aws_lambda_powertools.event_handler.graphql_appsync.exceptions import InvalidBatchResponse, ResolverNotFoundError
+from aws_lambda_powertools.event_handler.graphql_appsync.router import Router
from aws_lambda_powertools.utilities.data_classes import AppSyncResolverEvent
from aws_lambda_powertools.utilities.typing import LambdaContext
+from aws_lambda_powertools.warnings import PowertoolsUserWarning
logger = logging.getLogger(__name__)
-AppSyncResolverEventT = TypeVar("AppSyncResolverEventT", bound=AppSyncResolverEvent)
-
-class BaseRouter:
- current_event: AppSyncResolverEventT # type: ignore[valid-type]
- lambda_context: LambdaContext
- context: dict
-
- def __init__(self):
- self._resolvers: dict = {}
-
- def resolver(self, type_name: str = "*", field_name: Optional[str] = None):
- """Registers the resolver for field_name
-
- Parameters
- ----------
- type_name : str
- Type name
- field_name : str
- Field name
- """
-
- def register_resolver(func):
- logger.debug(f"Adding resolver `{func.__name__}` for field `{type_name}.{field_name}`")
- self._resolvers[f"{type_name}.{field_name}"] = {"func": func}
- return func
-
- return register_resolver
-
- 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()
-
-
-class AppSyncResolver(BaseRouter):
+class AppSyncResolver(Router):
"""
- AppSync resolver decorator
+ AppSync GraphQL API Resolver
Example
-------
-
- **Sample usage**
-
- from aws_lambda_powertools.event_handler import AppSyncResolver
-
- app = AppSyncResolver()
-
- @app.resolver(type_name="Query", field_name="listLocations")
- def list_locations(page: int = 0, size: int = 10) -> list:
- # Your logic to fetch locations with arguments passed in
- return [{"id": 100, "name": "Smooth Grooves"}]
-
- @app.resolver(type_name="Merchant", field_name="extraInfo")
- def get_extra_info() -> dict:
- # Can use "app.current_event.source" to filter within the parent context
- account_type = app.current_event.source["accountType"]
- method = "BTC" if account_type == "NEW" else "USD"
- return {"preferredPaymentMethod": method}
-
- @app.resolver(field_name="commonField")
- def common_field() -> str:
- # Would match all fieldNames matching 'commonField'
- return str(uuid.uuid4())
+ ```python
+ from aws_lambda_powertools.event_handler import AppSyncResolver
+
+ app = AppSyncResolver()
+
+ @app.resolver(type_name="Query", field_name="listLocations")
+ def list_locations(page: int = 0, size: int = 10) -> list:
+ # Your logic to fetch locations with arguments passed in
+ return [{"id": 100, "name": "Smooth Grooves"}]
+
+ @app.resolver(type_name="Merchant", field_name="extraInfo")
+ def get_extra_info() -> dict:
+ # Can use "app.current_event.source" to filter within the parent context
+ account_type = app.current_event.source["accountType"]
+ method = "BTC" if account_type == "NEW" else "USD"
+ return {"preferredPaymentMethod": method}
+
+ @app.resolver(field_name="commonField")
+ def common_field() -> str:
+ # Would match all fieldNames matching 'commonField'
+ return str(uuid.uuid4())
+ ```
"""
def __init__(self):
+ """
+ Initialize a new instance of the AppSyncResolver.
+ """
super().__init__()
self.context = {} # early init as customers might add context before event resolution
- def resolve(
+ self.current_batch_event: List[AppSyncResolverEvent] = []
+ self.current_event: Optional[AppSyncResolverEvent] = None
+ self.lambda_context: Optional[LambdaContext] = None
+
+ def __call__(
self,
event: dict,
context: LambdaContext,
data_model: Type[AppSyncResolverEvent] = AppSyncResolverEvent,
) -> Any:
- """Resolve field_name
+ """Implicit lambda handler which internally calls `resolve`"""
+ return self.resolve(event, context, data_model)
+
+ def resolve(
+ self,
+ event: Union[dict, List[Dict]],
+ context: LambdaContext,
+ data_model: Type[AppSyncResolverEvent] = AppSyncResolverEvent,
+ ) -> Any:
+ """Resolves the response based on the provide event and decorator routes
Parameters
----------
- event : dict
- Lambda event
+ event : dict | List[Dict]
+ Lambda event either coming from batch processing endpoint or from standard processing endpoint
context : LambdaContext
Lambda context
data_model:
@@ -154,45 +137,211 @@ def lambda_handler(event, context):
ValueError
If we could not find a field resolver
"""
- # Maintenance: revisit generics/overload to fix [attr-defined] in mypy usage
- BaseRouter.current_event = data_model(event)
- BaseRouter.lambda_context = context
- resolver = self._get_resolver(BaseRouter.current_event.type_name, BaseRouter.current_event.field_name)
- response = resolver(**BaseRouter.current_event.arguments)
+ self.lambda_context = context
+
+ if isinstance(event, list):
+ response = self._call_batch_resolver(event=event, data_model=data_model)
+ else:
+ response = self._call_single_resolver(event=event, data_model=data_model)
+
self.clear_context()
return response
- def _get_resolver(self, type_name: str, field_name: str) -> Callable:
- """Get resolver for field_name
+ def _call_single_resolver(self, event: dict, data_model: Type[AppSyncResolverEvent]) -> Any:
+ """Call single event resolver
Parameters
----------
- type_name : str
- Type name
- field_name : str
- Field name
+ event : dict
+ Event
+ data_model : Type[AppSyncResolverEvent]
+ Data_model to decode AppSync event, by default it is of AppSyncResolverEvent type or subclass of it
+ """
+
+ logger.debug("Processing direct resolver event")
+
+ self.current_event = data_model(event)
+ resolver = self._resolver_registry.find_resolver(self.current_event.type_name, self.current_event.field_name)
+ if not resolver:
+ raise ValueError(f"No resolver found for '{self.current_event.type_name}.{self.current_event.field_name}'")
+ return resolver["func"](**self.current_event.arguments)
+
+ def _call_sync_batch_resolver(
+ self,
+ resolver: Callable,
+ raise_on_error: bool = False,
+ aggregate: bool = True,
+ ) -> List[Any]:
+ """
+ Calls a synchronous batch resolver function for each event in the current batch.
+
+ Parameters
+ ----------
+ resolver: Callable
+ The callable function to resolve events.
+ 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.
Returns
-------
- Callable
- callable function and configuration
+ List[Any]
+ A list of results corresponding to the resolved events.
"""
- full_name = f"{type_name}.{field_name}"
- resolver = self._resolvers.get(full_name, self._resolvers.get(f"*.{field_name}"))
- if not resolver:
- raise ValueError(f"No resolver found for '{full_name}'")
- return resolver["func"]
- def __call__(
+ logger.debug(f"Graceful error handling flag {raise_on_error=}")
+
+ # Checks whether the entire batch should be processed at once
+ if aggregate:
+ # Process the entire batch
+ response = resolver(event=self.current_batch_event)
+
+ if not isinstance(response, List):
+ raise InvalidBatchResponse("The response must be a List when using batch resolvers")
+
+ return response
+
+ # Non aggregated events, so we call this event list x times
+ # Stop on first exception we encounter
+ if raise_on_error:
+ return [
+ resolver(event=appconfig_event, **appconfig_event.arguments)
+ for appconfig_event in self.current_batch_event
+ ]
+
+ # By default, we gracefully append `None` for any records that failed processing
+ results = []
+ for idx, event in enumerate(self.current_batch_event):
+ try:
+ results.append(resolver(event=event, **event.arguments))
+ except Exception:
+ logger.debug(f"Failed to process event number {idx} from field '{event.info.field_name}'")
+ results.append(None)
+
+ return results
+
+ async def _call_async_batch_resolver(
self,
- event: dict,
- context: LambdaContext,
- data_model: Type[AppSyncResolverEvent] = AppSyncResolverEvent,
- ) -> Any:
- """Implicit lambda handler which internally calls `resolve`"""
- return self.resolve(event, context, data_model)
+ resolver: Callable,
+ raise_on_error: bool = False,
+ aggregate: bool = True,
+ ) -> List[Any]:
+ """
+ Asynchronously call a batch resolver for each event in the current batch.
+
+ Parameters
+ ----------
+ resolver: Callable
+ The asynchronous resolver function.
+ 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.
+
+ Returns
+ -------
+ List[Any]
+ A list of results corresponding to the resolved events.
+ """
+
+ logger.debug(f"Graceful error handling flag {raise_on_error=}")
+
+ # Checks whether the entire batch should be processed at once
+ if aggregate:
+ # Process the entire batch
+ ret = await resolver(event=self.current_batch_event)
+ if not isinstance(ret, List):
+ raise InvalidBatchResponse("The response must be a List when using batch resolvers")
+
+ return ret
+
+ response: List = []
+
+ # Prime coroutines
+ tasks = [resolver(event=e, **e.arguments) for e in self.current_batch_event]
+
+ # Aggregate results or raise at first error
+ if raise_on_error:
+ response.extend(await asyncio.gather(*tasks))
+ return response
+
+ # Aggregate results and exceptions, then filter them out
+ # Use `None` upon exception for graceful error handling at GraphQL engine level
+ #
+ # NOTE: asyncio.gather(return_exceptions=True) catches and includes exceptions in the results
+ # this will become useful when we support exception handling in AppSync resolver
+ results = await asyncio.gather(*tasks, return_exceptions=True)
+ response.extend(None if isinstance(ret, Exception) else ret for ret in results)
+
+ return response
+
+ def _call_batch_resolver(self, event: List[dict], data_model: Type[AppSyncResolverEvent]) -> List[Any]:
+ """Call batch event resolver for sync and async methods
+
+ Parameters
+ ----------
+ event : List[dict]
+ Batch event
+ data_model : Type[AppSyncResolverEvent]
+ Data_model to decode AppSync event, by default AppSyncResolverEvent or a subclass
+
+ Returns
+ -------
+ List[Any]
+ Results of the resolver execution.
+
+ Raises
+ ------
+ InconsistentPayloadError:
+ When all events in the batch do not have the same fieldName.
+
+ ResolverNotFoundError:
+ When no resolver is found for the specified type and field.
+ """
+ logger.debug("Processing batch resolver event")
+
+ self.current_batch_event = [data_model(e) for e in event]
+ type_name, field_name = self.current_batch_event[0].type_name, self.current_batch_event[0].field_name
+
+ resolver = self._batch_resolver_registry.find_resolver(type_name, field_name)
+ async_resolver = self._async_batch_resolver_registry.find_resolver(type_name, field_name)
+
+ if resolver and async_resolver:
+ warnings.warn(
+ f"Both synchronous and asynchronous resolvers found for the same event and field."
+ f"The synchronous resolver takes precedence. Executing: {resolver['func'].__name__}",
+ stacklevel=2,
+ category=PowertoolsUserWarning,
+ )
+
+ if resolver:
+ logger.debug(f"Found sync resolver. {resolver=}, {field_name=}")
+ return self._call_sync_batch_resolver(
+ resolver=resolver["func"],
+ raise_on_error=resolver["raise_on_error"],
+ aggregate=resolver["aggregate"],
+ )
+
+ if async_resolver:
+ logger.debug(f"Found async resolver. {resolver=}, {field_name=}")
+ return asyncio.run(
+ self._call_async_batch_resolver(
+ resolver=async_resolver["func"],
+ raise_on_error=async_resolver["raise_on_error"],
+ aggregate=async_resolver["aggregate"],
+ ),
+ )
+
+ raise ResolverNotFoundError(f"No resolver found for '{type_name}.{field_name}'")
def include_router(self, router: "Router") -> None:
"""Adds all resolvers defined in a router
@@ -202,15 +351,112 @@ def include_router(self, router: "Router") -> None:
router : Router
A router containing a dict of field resolvers
"""
+
# Merge app and router context
+ logger.debug("Merging router and app context")
self.context.update(**router.context)
+
# use pointer to allow context clearance after event is processed e.g., resolve(evt, ctx)
router.context = self.context
- self._resolvers.update(router._resolvers)
+ logger.debug("Merging router resolver registries")
+ self._resolver_registry.merge(router._resolver_registry)
+ self._batch_resolver_registry.merge(router._batch_resolver_registry)
+ self._async_batch_resolver_registry.merge(router._async_batch_resolver_registry)
+ def resolver(self, type_name: str = "*", field_name: Optional[str] = None) -> Callable:
+ """Registers direct resolver function for GraphQL type and field name.
-class Router(BaseRouter):
- def __init__(self):
- super().__init__()
- self.context = {} # early init as customers might add context before event resolution
+ Parameters
+ ----------
+ type_name : str, optional
+ GraphQL type e.g., Query, Mutation, by default "*" meaning any
+ field_name : Optional[str], optional
+ GraphQL field e.g., getTodo, createTodo, by default None
+
+ Returns
+ -------
+ Callable
+ Registered resolver
+
+ Example
+ -------
+
+ ```python
+ from aws_lambda_powertools.event_handler import AppSyncResolver
+
+ from typing import TypedDict
+
+ app = AppSyncResolver()
+
+ class Todo(TypedDict, total=False):
+ id: str
+ userId: str
+ title: str
+ completed: bool
+
+ # resolve any GraphQL `getTodo` queries
+ # arguments are injected as function arguments as-is
+ @app.resolver(type_name="Query", field_name="getTodo")
+ def get_todo(id: str = "", status: str = "open") -> Todo:
+ todos: Response = requests.get(f"https://jsonplaceholder.typicode.com/todos/{id}")
+ todos.raise_for_status()
+
+ return todos.json()
+
+ def lambda_handler(event, context):
+ return app.resolve(event, context)
+ ```
+ """
+ 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:
+ """Registers batch resolver function for GraphQL type and field name.
+
+ By default, we handle errors gracefully by returning `None`. If you want
+ to short-circuit and fail the entire batch use `raise_on_error=True`.
+
+ Parameters
+ ----------
+ type_name : str, optional
+ GraphQL type e.g., Query, Mutation, by default "*" meaning any
+ field_name : Optional[str], optional
+ GraphQL field e.g., getTodo, createTodo, by default None
+ raise_on_error : bool, optional
+ Whether to fail entire batch upon error, or handle errors gracefully (None), by default False
+ 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.
+
+ Returns
+ -------
+ Callable
+ Registered resolver
+ """
+ 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,
+ )
diff --git a/tests/functional/feature_flags/__init__.py b/aws_lambda_powertools/event_handler/graphql_appsync/__init__.py
similarity index 100%
rename from tests/functional/feature_flags/__init__.py
rename to aws_lambda_powertools/event_handler/graphql_appsync/__init__.py
diff --git a/aws_lambda_powertools/event_handler/graphql_appsync/_registry.py b/aws_lambda_powertools/event_handler/graphql_appsync/_registry.py
new file mode 100644
index 00000000000..fe86f502d65
--- /dev/null
+++ b/aws_lambda_powertools/event_handler/graphql_appsync/_registry.py
@@ -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)
diff --git a/aws_lambda_powertools/event_handler/graphql_appsync/base.py b/aws_lambda_powertools/event_handler/graphql_appsync/base.py
new file mode 100644
index 00000000000..ad32d0d17f8
--- /dev/null
+++ b/aws_lambda_powertools/event_handler/graphql_appsync/base.py
@@ -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
diff --git a/aws_lambda_powertools/event_handler/graphql_appsync/exceptions.py b/aws_lambda_powertools/event_handler/graphql_appsync/exceptions.py
new file mode 100644
index 00000000000..f98a75b6f17
--- /dev/null
+++ b/aws_lambda_powertools/event_handler/graphql_appsync/exceptions.py
@@ -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
+ """
diff --git a/aws_lambda_powertools/event_handler/graphql_appsync/router.py b/aws_lambda_powertools/event_handler/graphql_appsync/router.py
new file mode 100644
index 00000000000..2046f09e03c
--- /dev/null
+++ b/aws_lambda_powertools/event_handler/graphql_appsync/router.py
@@ -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()
diff --git a/aws_lambda_powertools/shared/version.py b/aws_lambda_powertools/shared/version.py
index 0d9dda5d9e7..ce06c897651 100644
--- a/aws_lambda_powertools/shared/version.py
+++ b/aws_lambda_powertools/shared/version.py
@@ -1,3 +1,3 @@
"""Exposes version constant to avoid circular dependencies."""
-VERSION = "2.39.0"
+VERSION = "2.40.0"
diff --git a/aws_lambda_powertools/utilities/data_classes/appsync_resolver_event.py b/aws_lambda_powertools/utilities/data_classes/appsync_resolver_event.py
index f58308377ff..d3166db1e3b 100644
--- a/aws_lambda_powertools/utilities/data_classes/appsync_resolver_event.py
+++ b/aws_lambda_powertools/utilities/data_classes/appsync_resolver_event.py
@@ -184,9 +184,9 @@ def identity(self) -> Union[None, AppSyncIdentityIAM, AppSyncIdentityCognito]:
return get_identity_object(self.get("identity"))
@property
- def source(self) -> Optional[Dict[str, Any]]:
+ def source(self) -> Dict[str, Any]:
"""A map that contains the resolution of the parent field."""
- return self.get("source")
+ return self.get("source") or {}
@property
def request_headers(self) -> Dict[str, str]:
diff --git a/aws_lambda_powertools/utilities/data_classes/kafka_event.py b/aws_lambda_powertools/utilities/data_classes/kafka_event.py
index f20c5254730..3b5a60c583c 100644
--- a/aws_lambda_powertools/utilities/data_classes/kafka_event.py
+++ b/aws_lambda_powertools/utilities/data_classes/kafka_event.py
@@ -15,12 +15,12 @@ def topic(self) -> str:
return self["topic"]
@property
- def partition(self) -> str:
+ def partition(self) -> int:
"""The Kafka record parition."""
return self["partition"]
@property
- def offset(self) -> str:
+ def offset(self) -> int:
"""The Kafka record offset."""
return self["offset"]
diff --git a/aws_lambda_powertools/utilities/parameters/secrets.py b/aws_lambda_powertools/utilities/parameters/secrets.py
index 0494c64985a..89df05f26e1 100644
--- a/aws_lambda_powertools/utilities/parameters/secrets.py
+++ b/aws_lambda_powertools/utilities/parameters/secrets.py
@@ -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:
diff --git a/aws_lambda_powertools/utilities/validation/base.py b/aws_lambda_powertools/utilities/validation/base.py
index 61d692d7f28..9d7a36874aa 100644
--- a/aws_lambda_powertools/utilities/validation/base.py
+++ b/aws_lambda_powertools/utilities/validation/base.py
@@ -3,12 +3,18 @@
import fastjsonschema # type: ignore
-from .exceptions import InvalidSchemaFormatError, SchemaValidationError
+from aws_lambda_powertools.utilities.validation.exceptions import InvalidSchemaFormatError, SchemaValidationError
logger = logging.getLogger(__name__)
-def validate_data_against_schema(data: Union[Dict, str], schema: Dict, formats: Optional[Dict] = None):
+def validate_data_against_schema(
+ data: Union[Dict, str],
+ schema: Dict,
+ formats: Optional[Dict] = None,
+ handlers: Optional[Dict] = None,
+ provider_options: Optional[Dict] = None,
+):
"""Validate dict data against given JSON Schema
Parameters
@@ -19,6 +25,11 @@ def validate_data_against_schema(data: Union[Dict, str], schema: Dict, formats:
JSON Schema to validate against
formats: Dict
Custom formats containing a key (e.g. int64) and a value expressed as regex or callback returning bool
+ handlers: Dict
+ Custom methods to retrieve remote schemes, keyed off of URI scheme
+ provider_options: Dict
+ Arguments that will be passed directly to the underlying validation call, in this case fastjsonchema.validate.
+ For all supported arguments see: https://horejsek.github.io/python-fastjsonschema/#fastjsonschema.validate
Raises
------
@@ -29,7 +40,9 @@ def validate_data_against_schema(data: Union[Dict, str], schema: Dict, formats:
"""
try:
formats = formats or {}
- fastjsonschema.validate(definition=schema, data=data, formats=formats)
+ handlers = handlers or {}
+ provider_options = provider_options or {}
+ fastjsonschema.validate(definition=schema, data=data, formats=formats, handlers=handlers, **provider_options)
except (TypeError, AttributeError, fastjsonschema.JsonSchemaDefinitionException) as e:
raise InvalidSchemaFormatError(f"Schema received: {schema}, Formats: {formats}. Error: {e}")
except fastjsonschema.JsonSchemaValueException as e:
diff --git a/aws_lambda_powertools/utilities/validation/exceptions.py b/aws_lambda_powertools/utilities/validation/exceptions.py
index 8789e3f2e80..c9ba0e2a75e 100644
--- a/aws_lambda_powertools/utilities/validation/exceptions.py
+++ b/aws_lambda_powertools/utilities/validation/exceptions.py
@@ -1,6 +1,6 @@
from typing import Any, List, Optional
-from ...exceptions import InvalidEnvelopeExpressionError
+from aws_lambda_powertools.exceptions import InvalidEnvelopeExpressionError
class SchemaValidationError(Exception):
diff --git a/aws_lambda_powertools/utilities/validation/validator.py b/aws_lambda_powertools/utilities/validation/validator.py
index 968656ee49c..74861f7de3f 100644
--- a/aws_lambda_powertools/utilities/validation/validator.py
+++ b/aws_lambda_powertools/utilities/validation/validator.py
@@ -1,10 +1,9 @@
import logging
from typing import Any, Callable, Dict, Optional, Union
+from aws_lambda_powertools.middleware_factory import lambda_handler_decorator
from aws_lambda_powertools.utilities import jmespath_utils
-
-from ...middleware_factory import lambda_handler_decorator
-from .base import validate_data_against_schema
+from aws_lambda_powertools.utilities.validation.base import validate_data_against_schema
logger = logging.getLogger(__name__)
@@ -16,8 +15,12 @@ def validator(
context: Any,
inbound_schema: Optional[Dict] = None,
inbound_formats: Optional[Dict] = None,
+ inbound_handlers: Optional[Dict] = None,
+ inbound_provider_options: Optional[Dict] = None,
outbound_schema: Optional[Dict] = None,
outbound_formats: Optional[Dict] = None,
+ outbound_handlers: Optional[Dict] = None,
+ outbound_provider_options: Optional[Dict] = None,
envelope: str = "",
jmespath_options: Optional[Dict] = None,
**kwargs: Any,
@@ -44,6 +47,17 @@ def validator(
Custom formats containing a key (e.g. int64) and a value expressed as regex or callback returning bool
outbound_formats: Dict
Custom formats containing a key (e.g. int64) and a value expressed as regex or callback returning bool
+ inbound_handlers: Dict
+ Custom methods to retrieve remote schemes, keyed off of URI scheme
+ outbound_handlers: Dict
+ Custom methods to retrieve remote schemes, keyed off of URI scheme
+ inbound_provider_options: Dict
+ Arguments that will be passed directly to the underlying validation call, in this case fastjsonchema.validate.
+ For all supported arguments see: https://horejsek.github.io/python-fastjsonschema/#fastjsonschema.validate
+ outbound_provider_options: Dict
+ Arguments that will be passed directly to the underlying validation call, in this case fastjsonchema.validate.
+ For all supported arguments see: https://horejsek.github.io/python-fastjsonschema/#fastjsonschema.validate
+
Example
-------
@@ -127,13 +141,25 @@ def handler(event, context):
if inbound_schema:
logger.debug("Validating inbound event")
- validate_data_against_schema(data=event, schema=inbound_schema, formats=inbound_formats)
+ validate_data_against_schema(
+ data=event,
+ schema=inbound_schema,
+ formats=inbound_formats,
+ handlers=inbound_handlers,
+ provider_options=inbound_provider_options,
+ )
response = handler(event, context, **kwargs)
if outbound_schema:
logger.debug("Validating outbound event")
- validate_data_against_schema(data=response, schema=outbound_schema, formats=outbound_formats)
+ validate_data_against_schema(
+ data=response,
+ schema=outbound_schema,
+ formats=outbound_formats,
+ handlers=outbound_handlers,
+ provider_options=outbound_provider_options,
+ )
return response
@@ -142,6 +168,8 @@ def validate(
event: Any,
schema: Dict,
formats: Optional[Dict] = None,
+ handlers: Optional[Dict] = None,
+ provider_options: Optional[Dict] = None,
envelope: Optional[str] = None,
jmespath_options: Optional[Dict] = None,
):
@@ -161,6 +189,10 @@ def validate(
Alternative JMESPath options to be included when filtering expr
formats: Dict
Custom formats containing a key (e.g. int64) and a value expressed as regex or callback returning bool
+ handlers: Dict
+ Custom methods to retrieve remote schemes, keyed off of URI scheme
+ provider_options: Dict
+ Arguments that will be passed directly to the underlying validate call
Example
-------
@@ -229,4 +261,10 @@ def handler(event, context):
jmespath_options=jmespath_options,
)
- validate_data_against_schema(data=event, schema=schema, formats=formats)
+ validate_data_against_schema(
+ data=event,
+ schema=schema,
+ formats=formats,
+ handlers=handlers,
+ provider_options=provider_options,
+ )
diff --git a/docs/Dockerfile b/docs/Dockerfile
index 7a276f1a2ec..3f2cc7c2bf9 100644
--- a/docs/Dockerfile
+++ b/docs/Dockerfile
@@ -1,5 +1,5 @@
# v9.1.18
-FROM squidfunk/mkdocs-material@sha256:96abcbb1bb6d4d79a6a142fe88150b3862b2b297fe45e6656b154d978b1787eb
+FROM squidfunk/mkdocs-material@sha256:257eca88da7f42242cd05e8cebf6d10ebd079edc207b089ad3f4f1ad107b0348
# pip-compile --generate-hashes --output-file=requirements.txt requirements.in
COPY requirements.txt /tmp/
RUN pip install --require-hashes -r /tmp/requirements.txt
diff --git a/docs/core/event_handler/appsync.md b/docs/core/event_handler/appsync.md
index fcadc2a1f27..440940cd107 100644
--- a/docs/core/event_handler/appsync.md
+++ b/docs/core/event_handler/appsync.md
@@ -3,40 +3,69 @@ title: GraphQL API
description: Core utility
---
-Event handler for AWS AppSync Direct Lambda Resolver and Amplify GraphQL Transformer.
+Event Handler for AWS AppSync and Amplify GraphQL Transformer.
+
+```mermaid
+stateDiagram-v2
+ direction LR
+ EventSource: AWS Lambda Event Sources
+ EventHandlerResolvers: AWS AppSync Direct invocation
AWS AppSync Batch invocation
+ LambdaInit: Lambda invocation
+ EventHandler: Event Handler
+ EventHandlerResolver: Route event based on GraphQL type/field keys
+ YourLogic: Run your registered resolver function
+ EventHandlerResolverBuilder: Adapts response to Event Source contract
+ LambdaResponse: Lambda response
+
+ state EventSource {
+ EventHandlerResolvers
+ }
+
+ EventHandlerResolvers --> LambdaInit
+
+ LambdaInit --> EventHandler
+ EventHandler --> EventHandlerResolver
+
+ state EventHandler {
+ [*] --> EventHandlerResolver: app.resolve(event, context)
+ EventHandlerResolver --> YourLogic
+ YourLogic --> EventHandlerResolverBuilder
+ }
+
+ EventHandler --> LambdaResponse
+```
## Key Features
-* Automatically parse API arguments to function arguments
* Choose between strictly match a GraphQL field name or all of them to a function
-* Integrates with [Data classes utilities](../../utilities/data_classes.md){target="_blank"} to access resolver and identity information
-* Works with both Direct Lambda Resolver and Amplify GraphQL Transformer `@function` directive
-* Support async Python 3.8+ functions, and generators
+* Automatically parse API arguments to function arguments
+* Integrates with [Event Source Data classes utilities](../../utilities/data_classes.md){target="_blank"} to access resolver and identity information
+* Support async Python 3.8+ functions and generators
## Terminology
**[Direct Lambda Resolver](https://docs.aws.amazon.com/appsync/latest/devguide/direct-lambda-reference.html){target="_blank"}**. A custom AppSync Resolver to bypass the use of Apache Velocity Template (VTL) and automatically map your function's response to a GraphQL field.
-**[Amplify GraphQL Transformer](https://docs.amplify.aws/cli/graphql-transformer/function){target="_blank"}**. Custom GraphQL directives to define your application's data model using Schema Definition Language (SDL). Amplify CLI uses these directives to convert GraphQL SDL into full descriptive AWS CloudFormation templates.
+**[Amplify GraphQL Transformer](https://docs.amplify.aws/cli/graphql-transformer/function){target="_blank"}**. Custom GraphQL directives to define your application's data model using Schema Definition Language _(SDL)_, _e.g., `@function`_. Amplify CLI uses these directives to convert GraphQL SDL into full descriptive AWS CloudFormation templates.
## Getting started
+???+ tip "Tip: Designing GraphQL Schemas for the first time?"
+ Visit [AWS AppSync schema documentation](https://docs.aws.amazon.com/appsync/latest/devguide/designing-your-schema.html){target="_blank"} to understand how to define types, nesting, and pagination.
+
### Required resources
-You must have an existing AppSync GraphQL API and IAM permissions to invoke your Lambda function. That said, there is no additional permissions to use this utility.
+You must have an existing AppSync GraphQL API and IAM permissions to invoke your Lambda function. That said, there is no additional permissions to use Event Handler as routing requires no dependency (_standard library_).
This is the sample infrastructure we are using for the initial examples with a AppSync Direct Lambda Resolver.
-???+ tip "Tip: Designing GraphQL Schemas for the first time?"
- Visit [AWS AppSync schema documentation](https://docs.aws.amazon.com/appsync/latest/devguide/designing-your-schema.html){target="_blank"} for understanding how to define types, nesting, and pagination.
-
=== "getting_started_schema.graphql"
```typescript
--8<-- "examples/event_handler_graphql/src/getting_started_schema.graphql"
```
-=== "template.yml"
+=== "template.yaml"
```yaml hl_lines="59-60 71-72 94-95 104-105 112-113"
--8<-- "examples/event_handler_graphql/sam/template.yaml"
@@ -259,6 +288,275 @@ You can use `append_context` when you want to share data between your App and Ro
--8<-- "examples/event_handler_graphql/src/split_operation_append_context_module.py"
```
+### Batch processing
+
+```mermaid
+stateDiagram-v2
+ direction LR
+ LambdaInit: Lambda invocation
+ EventHandler: Event Handler
+ EventHandlerResolver: Route event based on GraphQL type/field keys
+ Client: Client query (listPosts)
+ YourLogic: Run your registered resolver function
+ EventHandlerResolverBuilder: Verifies response is a list
+ AppSyncBatchPostsResolution: query listPosts
+ AppSyncBatchPostsItems: get all posts data (id, title, relatedPosts)
+ AppSyncBatchRelatedPosts: get related posts (id, title, relatedPosts)
+ AppSyncBatchAggregate: aggregate batch resolver event
+ AppSyncBatchLimit: reached batch size limit
+ LambdaResponse: Lambda response
+
+ Client --> AppSyncBatchResolverMode
+ state AppSyncBatchResolverMode {
+ [*] --> AppSyncBatchPostsResolution
+ AppSyncBatchPostsResolution --> AppSyncBatchPostsItems
+ AppSyncBatchPostsItems --> AppSyncBatchRelatedPosts: N additional queries
+ AppSyncBatchRelatedPosts --> AppSyncBatchRelatedPosts
+ AppSyncBatchRelatedPosts --> AppSyncBatchAggregate
+ AppSyncBatchRelatedPosts --> AppSyncBatchAggregate
+ AppSyncBatchRelatedPosts --> AppSyncBatchAggregate
+ AppSyncBatchAggregate --> AppSyncBatchLimit
+ }
+
+ AppSyncBatchResolverMode --> LambdaInit: 1x Invoke with N events
+ LambdaInit --> EventHandler
+
+ state EventHandler {
+ [*] --> EventHandlerResolver: app.resolve(event, context)
+ EventHandlerResolver --> YourLogic
+ YourLogic --> EventHandlerResolverBuilder
+ EventHandlerResolverBuilder --> LambdaResponse
+ }
+```
+
+Batch resolvers mechanics: visualizing N+1 in `relatedPosts` field.
+
+#### Understanding N+1 problem
+
+When AWS AppSync has [batching enabled for Lambda Resolvers](https://docs.aws.amazon.com/appsync/latest/devguide/tutorial-lambda-resolvers.html#advanced-use-case-batching){target="_blank"}, it will group as many requests as possible before invoking your Lambda invocation. Effectively solving the [N+1 problem in GraphQL](https://aws.amazon.com/blogs/mobile/introducing-configurable-batching-size-for-aws-appsync-lambda-resolvers/){target="_blank"}.
+
+For example, say you have a query named `listPosts`. For each post, you also want `relatedPosts`. **Without batching**, AppSync will:
+
+1. Invoke your Lambda function to get the first post
+2. Invoke your Lambda function for each related post
+3. Repeat 1 until done
+
+```mermaid
+sequenceDiagram
+ participant Client
+ participant AppSync
+ participant Lambda
+ participant Database
+
+ Client->>AppSync: GraphQL Query
+ Note over Client,AppSync: query listPosts {
id
title
relatedPosts { id title }
}
+
+ AppSync->>Lambda: Fetch N posts (listPosts)
+ Lambda->>Database: Query
+ Database->>Lambda: Posts
+ Lambda-->>AppSync: Return posts (id, title)
+ loop Fetch N related posts (relatedPosts)
+ AppSync->>Lambda: Invoke function (N times)
+ Lambda->>Database: Query
+ Database-->>Lambda: Return related posts
+ Lambda-->>AppSync: Return related posts
+ end
+ AppSync-->>Client: Return posts and their related posts
+```
+
+#### Batch resolvers
+
+You can use `@batch_resolver` or `@async_batch_resolver` decorators to receive the entire batch of requests.
+
+In this mode, you must return results in the same order of your batch items, so AppSync can associate the results back to the client.
+
+=== "advanced_batch_resolver.py"
+ ```python hl_lines="5 9 23"
+ --8<-- "examples/event_handler_graphql/src/advanced_batch_resolver.py"
+ ```
+
+ 1. The entire batch is sent to the resolver. You need to iterate through it to process all records.
+ 2. We use `post_id` as our unique identifier of the GraphQL request.
+
+=== "advanced_batch_resolver_payload.json"
+ ```json hl_lines="6 16 25 35 44 54"
+ --8<-- "examples/event_handler_graphql/src/advanced_batch_resolver_payload.json"
+ ```
+
+=== "advanced_batch_query.graphql"
+ ```typescript hl_lines="3 6"
+ --8<-- "examples/event_handler_graphql/src/advanced_batch_query.graphql"
+ ```
+
+##### Processing items individually
+
+```mermaid
+stateDiagram-v2
+ direction LR
+ LambdaInit: Lambda invocation
+ EventHandler: Event Handler
+ EventHandlerResolver: Route event based on GraphQL type/field keys
+ Client: Client query (listPosts)
+ YourLogic: Call your registered resolver function N times
+ EventHandlerResolverErrorHandling: Gracefully handle errors with null response
+ EventHandlerResolverBuilder: Aggregate responses to match batch size
+ AppSyncBatchPostsResolution: query listPosts
+ AppSyncBatchPostsItems: get all posts data (id, title, relatedPosts)
+ AppSyncBatchRelatedPosts: get related posts (id, title, relatedPosts)
+ AppSyncBatchAggregate: aggregate batch resolver event
+ AppSyncBatchLimit: reached batch size limit
+ LambdaResponse: Lambda response
+
+ Client --> AppSyncBatchResolverMode
+ state AppSyncBatchResolverMode {
+ [*] --> AppSyncBatchPostsResolution
+ AppSyncBatchPostsResolution --> AppSyncBatchPostsItems
+ AppSyncBatchPostsItems --> AppSyncBatchRelatedPosts: N additional queries
+ AppSyncBatchRelatedPosts --> AppSyncBatchRelatedPosts
+ AppSyncBatchRelatedPosts --> AppSyncBatchAggregate
+ AppSyncBatchRelatedPosts --> AppSyncBatchAggregate
+ AppSyncBatchRelatedPosts --> AppSyncBatchAggregate
+ AppSyncBatchAggregate --> AppSyncBatchLimit
+ }
+
+ AppSyncBatchResolverMode --> LambdaInit: 1x Invoke with N events
+ LambdaInit --> EventHandler
+
+ state EventHandler {
+ [*] --> EventHandlerResolver: app.resolve(event, context)
+ EventHandlerResolver --> YourLogic
+ YourLogic --> EventHandlerResolverErrorHandling
+ EventHandlerResolverErrorHandling --> EventHandlerResolverBuilder
+ EventHandlerResolverBuilder --> LambdaResponse
+ }
+```
+
+Batch resolvers: reducing Lambda invokes but fetching data N times (similar to single resolver).
+
+In rare scenarios, you might want to process each item individually, trading ease of use for increased latency as you handle one batch item at a time.
+
+You can toggle `aggregate` parameter in `@batch_resolver` decorator for your resolver function to be called N times.
+
+!!! note "This does not resolve the N+1 problem, but shifts it to the Lambda runtime."
+
+In this mode, we will:
+
+1. Aggregate each response we receive from your function in the exact order it receives
+2. Gracefully handle errors by adding `None` in the final response for each batch item that failed processing
+ * You can customize `nul` or error responses back to the client in the [AppSync resolver mapping templates](https://docs.aws.amazon.com/appsync/latest/devguide/tutorial-lambda-resolvers.html#returning-individual-errors){target="_blank"}
+
+=== "advanced_batch_resolver_individual.py"
+ ```python hl_lines="5 9 19"
+ --8<-- "examples/event_handler_graphql/src/advanced_batch_resolver_individual.py"
+ ```
+
+ 1. You need to disable the aggregated event by using `aggregate` flag.
+ The resolver receives and processes each record one at a time.
+
+=== "advanced_batch_resolver_payload.json"
+ ```json hl_lines="6 16 25 35 44 54"
+ --8<-- "examples/event_handler_graphql/src/advanced_batch_resolver_payload.json"
+ ```
+
+=== "advanced_batch_query.graphql"
+ ```typescript hl_lines="3 6"
+ --8<-- "examples/event_handler_graphql/src/advanced_batch_query.graphql"
+ ```
+
+##### Raise on error
+
+```mermaid
+stateDiagram-v2
+ direction LR
+ LambdaInit: Lambda invocation
+ EventHandler: Event Handler
+ EventHandlerResolver: Route event based on GraphQL type/field keys
+ Client: Client query (listPosts)
+ YourLogic: Call your registered resolver function N times
+ EventHandlerResolverErrorHandling: Error?
+ EventHandlerResolverHappyPath: No error?
+ EventHandlerResolverUnhappyPath: Propagate any exception
+ EventHandlerResolverBuilder: Aggregate responses to match batch size
+ AppSyncBatchPostsResolution: query listPosts
+ AppSyncBatchPostsItems: get all posts data (id, title, relatedPosts)
+ AppSyncBatchRelatedPosts: get related posts (id, title, relatedPosts)
+ AppSyncBatchAggregate: aggregate batch resolver event
+ AppSyncBatchLimit: reached batch size limit
+ LambdaResponse: Lambda response
+ LambdaErrorResponse: Lambda error
+
+ Client --> AppSyncBatchResolverMode
+ state AppSyncBatchResolverMode {
+ [*] --> AppSyncBatchPostsResolution
+ AppSyncBatchPostsResolution --> AppSyncBatchPostsItems
+ AppSyncBatchPostsItems --> AppSyncBatchRelatedPosts: N additional queries
+ AppSyncBatchRelatedPosts --> AppSyncBatchRelatedPosts
+ AppSyncBatchRelatedPosts --> AppSyncBatchAggregate
+ AppSyncBatchRelatedPosts --> AppSyncBatchAggregate
+ AppSyncBatchRelatedPosts --> AppSyncBatchAggregate
+ AppSyncBatchAggregate --> AppSyncBatchLimit
+ }
+
+ AppSyncBatchResolverMode --> LambdaInit: 1x Invoke with N events
+ LambdaInit --> EventHandler
+
+ state EventHandler {
+ [*] --> EventHandlerResolver: app.resolve(event, context)
+ EventHandlerResolver --> YourLogic
+ YourLogic --> EventHandlerResolverHappyPath
+ YourLogic --> EventHandlerResolverErrorHandling
+ EventHandlerResolverHappyPath --> EventHandlerResolverBuilder
+ EventHandlerResolverErrorHandling --> EventHandlerResolverUnhappyPath
+ EventHandlerResolverUnhappyPath --> LambdaErrorResponse
+
+ EventHandlerResolverBuilder --> LambdaResponse
+ }
+```
+
+Batch resolvers: reducing Lambda invokes but fetching data N times (similar to single resolver).
+
+You can toggle `raise_on_error` parameter in `@batch_resolver` to propagate any exception instead of gracefully returning `None` for a given batch item.
+
+This is useful when you want to stop processing immediately in the event of an unhandled or unrecoverable exception.
+
+=== "advanced_batch_resolver_handling_error.py"
+ ```python hl_lines="5 9 19"
+ --8<-- "examples/event_handler_graphql/src/advanced_batch_resolver_handling_error.py"
+ ```
+
+ 1. You can enable enable the error handling by using `raise_on_error` flag.
+
+=== "advanced_batch_resolver_payload.json"
+ ```json hl_lines="6 16 25 35 44 54"
+ --8<-- "examples/event_handler_graphql/src/advanced_batch_resolver_payload.json"
+ ```
+
+=== "advanced_batch_query.graphql"
+ ```typescript hl_lines="3 6"
+ --8<-- "examples/event_handler_graphql/src/advanced_batch_query.graphql"
+ ```
+
+#### Async batch resolver
+
+Similar to `@batch_resolver` explained in [batch resolvers](#batch-resolvers), you can use `async_batch_resolver` to handle async functions.
+
+=== "advanced_batch_async_resolver.py"
+ ```python hl_lines="5 9 23"
+ --8<-- "examples/event_handler_graphql/src/advanced_batch_async_resolver.py"
+ ```
+
+ 1. `async_batch_resolver` takes care of running and waiting for coroutine completion.
+
+=== "advanced_batch_resolver_payload.json"
+ ```json hl_lines="6 16 25 35 44 54"
+ --8<-- "examples/event_handler_graphql/src/advanced_batch_resolver_payload.json"
+ ```
+
+=== "advanced_batch_query.graphql"
+ ```typescript hl_lines="3 6"
+ --8<-- "examples/event_handler_graphql/src/advanced_batch_query.graphql"
+ ```
+
## Testing your code
You can test your resolvers by passing a mocked or actual AppSync Lambda event that you're expecting.
diff --git a/docs/index.md b/docs/index.md
index 1bf9dec2574..2fd0b9e430c 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -63,14 +63,98 @@ You can install Powertools for AWS Lambda (Python) using your favorite dependenc
=== "Lambda Layer"
- You can add our layer both in the [AWS Lambda Console _(under `Layers`)_](https://eu-west-1.console.aws.amazon.com/lambda/home#/add/layer){target="_blank"}, or via your favorite infrastructure as code framework with the ARN value.
-
- For the latter, make sure to replace `{region}` with your AWS region, e.g., `eu-west-1`.
-
- * x86 architecture: __arn:aws:lambda:{region}:017000801446:layer:AWSLambdaPowertoolsPythonV2:72__{: .copyMe}:clipboard:
- * ARM architecture: __arn:aws:lambda:{region}:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:72__{: .copyMe}:clipboard:
-
- ???+ note "Code snippets for popular infrastructure as code frameworks"
+ [Lambda Layer](https://docs.aws.amazon.com/lambda/latest/dg/configuration-layers.html){target="_blank"} is a .zip file archive that can contain additional code, pre-packaged dependencies, data, or configuration files. We compile and optimize [all dependencies](#install), and remove duplicate dependencies [already available in the Lambda runtime](https://github.com/aws-powertools/powertools-lambda-layer-cdk/blob/d24716744f7d1f37617b4998c992c4c067e19e64/layer/Python/Dockerfile#L36){target="_blank"} to achieve the most optimal size.
+
+ | Architecture | Layer ARN |
+ | ------------ | --------------------------------------------------------------------------------------------------------- |
+ | x86 | __arn:aws:lambda:{region}:017000801446:layer:AWSLambdaPowertoolsPythonV2:73__{: .copyMe}:clipboard: |
+ | ARM | __arn:aws:lambda:{region}:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:73__{: .copyMe}:clipboard: |
+
+ === "AWS Console"
+
+ You can add our layer using the [AWS Lambda Console _(direct link)_](https://console.aws.amazon.com/lambda/home#/add/layer){target="_blank"}:
+
+ * Under Layers, choose `Specify an ARN`
+ * Click to copy the correct ARN value based on your AWS Lambda function architecture and region
+
+ ??? info "Click to expand and copy any regional Lambda Layer ARN"
+
+ === "x86_64"
+
+
+ | Region name | Region code | Layer ARN |
+ | ------------------------- | -------------------- | --------------------------------------------------------------------------------------------------------- |
+ | Africa (Cape Town) | **`af-south-1`** | **arn:aws:lambda:af-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV2:73**{: .copyMe}:clipboard: |
+ | Asia Pacific (Hong Kong) | **`ap-east-1`** | **arn:aws:lambda:ap-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV2:73**{: .copyMe}:clipboard: |
+ | Asia Pacific (Tokyo) | **`ap-northeast-1`** | **arn:aws:lambda:ap-northeast-1:017000801446:layer:AWSLambdaPowertoolsPythonV2:73**{: .copyMe}:clipboard: |
+ | Asia Pacific (Seoul) | **`ap-northeast-2`** | **arn:aws:lambda:ap-northeast-2:017000801446:layer:AWSLambdaPowertoolsPythonV2:73**{: .copyMe}:clipboard: |
+ | Asia Pacific (Osaka) | **`ap-northeast-3`** | **arn:aws:lambda:ap-northeast-3:017000801446:layer:AWSLambdaPowertoolsPythonV2:73**{: .copyMe}:clipboard: |
+ | Asia Pacific (Mumbai) | **`ap-south-1`** | **arn:aws:lambda:ap-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV2:73**{: .copyMe}:clipboard: |
+ | Asia Pacific (Hyderabad) | **`ap-south-2`** | **arn:aws:lambda:ap-south-2:017000801446:layer:AWSLambdaPowertoolsPythonV2:73**{: .copyMe}:clipboard: |
+ | Asia Pacific (Singapore) | **`ap-southeast-1`** | **arn:aws:lambda:ap-southeast-1:017000801446:layer:AWSLambdaPowertoolsPythonV2:73**{: .copyMe}:clipboard: |
+ | Asia Pacific (Sydney) | **`ap-southeast-2`** | **arn:aws:lambda:ap-southeast-2:017000801446:layer:AWSLambdaPowertoolsPythonV2:73**{: .copyMe}:clipboard: |
+ | Asia Pacific (Jakarta) | **`ap-southeast-3`** | **arn:aws:lambda:ap-southeast-3:017000801446:layer:AWSLambdaPowertoolsPythonV2:73**{: .copyMe}:clipboard: |
+ | Asia Pacific (Melbourne) | **`ap-southeast-4`** | **arn:aws:lambda:ap-southeast-4:017000801446:layer:AWSLambdaPowertoolsPythonV2:73**{: .copyMe}:clipboard: |
+ | Canada (Central) | **`ca-central-1`** | **arn:aws:lambda:ca-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV2:73**{: .copyMe}:clipboard: |
+ | Canada (West) | **`ca-west-1`** | **arn:aws:lambda:ca-west-1:017000801446:layer:AWSLambdaPowertoolsPythonV2:73**{: .copyMe}:clipboard: |
+ | Europe (Frankfurt) | **`eu-central-1`** | **arn:aws:lambda:eu-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV2:73**{: .copyMe}:clipboard: |
+ | Europe (Zurich) | **`eu-central-2`** | **arn:aws:lambda:eu-central-2:017000801446:layer:AWSLambdaPowertoolsPythonV2:73**{: .copyMe}:clipboard: |
+ | Europe (Stockholm) | **`eu-north-1`** | **arn:aws:lambda:eu-north-1:017000801446:layer:AWSLambdaPowertoolsPythonV2:73**{: .copyMe}:clipboard: |
+ | Europe (Milan) | **`eu-south-1`** | **arn:aws:lambda:eu-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV2:73**{: .copyMe}:clipboard: |
+ | Europe (Spain) | **`eu-south-2`** | **arn:aws:lambda:eu-south-2:017000801446:layer:AWSLambdaPowertoolsPythonV2:73**{: .copyMe}:clipboard: |
+ | Europe (Ireland) | **`eu-west-1`** | **arn:aws:lambda:eu-west-1:017000801446:layer:AWSLambdaPowertoolsPythonV2:73**{: .copyMe}:clipboard: |
+ | Europe (London) | **`eu-west-2`** | **arn:aws:lambda:eu-west-2:017000801446:layer:AWSLambdaPowertoolsPythonV2:73**{: .copyMe}:clipboard: |
+ | Europe (Paris) | **`eu-west-3`** | **arn:aws:lambda:eu-west-3:017000801446:layer:AWSLambdaPowertoolsPythonV2:73**{: .copyMe}:clipboard: |
+ | Middle East (Israel) | **`il-central-1`** | **arn:aws:lambda:il-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV2:73**{: .copyMe}:clipboard: |
+ | Middle East (UAE) | **`me-central-1`** | **arn:aws:lambda:me-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV2:73**{: .copyMe}:clipboard: |
+ | Middle East (Bahrain) | **`me-south-1`** | **arn:aws:lambda:me-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV2:73**{: .copyMe}:clipboard: |
+ | South America (São Paulo) | **`sa-east-1`** | **arn:aws:lambda:sa-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV2:73**{: .copyMe}:clipboard: |
+ | US East (N. Virginia) | **`us-east-1`** | **arn:aws:lambda:us-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV2:73**{: .copyMe}:clipboard: |
+ | US East (Ohio) | **`us-east-2`** | **arn:aws:lambda:us-east-2:017000801446:layer:AWSLambdaPowertoolsPythonV2:73**{: .copyMe}:clipboard: |
+ | US West (N. California) | **`us-west-1`** | **arn:aws:lambda:us-west-1:017000801446:layer:AWSLambdaPowertoolsPythonV2:73**{: .copyMe}:clipboard: |
+ | US West (Oregon) | **`us-west-2`** | **arn:aws:lambda:us-west-2:017000801446:layer:AWSLambdaPowertoolsPythonV2:73**{: .copyMe}:clipboard: |
+
+
+ === "arm64"
+
+ | Region name | Region code | Layer ARN |
+ | ------------------------- | -------------------- | --------------------------------------------------------------------------------------------------------------- |
+ | Africa (Cape Town) | **`af-south-1`** | **arn:aws:lambda:af-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:73**{: .copyMe}:clipboard: |
+ | Asia Pacific (Hong Kong) | **`ap-east-1`** | **arn:aws:lambda:ap-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:73**{: .copyMe}:clipboard: |
+ | Asia Pacific (Tokyo) | **`ap-northeast-1`** | **arn:aws:lambda:ap-northeast-1:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:73**{: .copyMe}:clipboard: |
+ | Asia Pacific (Seoul) | **`ap-northeast-2`** | **arn:aws:lambda:ap-northeast-2:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:73**{: .copyMe}:clipboard: |
+ | Asia Pacific (Osaka) | **`ap-northeast-3`** | **arn:aws:lambda:ap-northeast-3:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:73**{: .copyMe}:clipboard: |
+ | Asia Pacific (Mumbai) | **`ap-south-1`** | **arn:aws:lambda:ap-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:73**{: .copyMe}:clipboard: |
+ | Asia Pacific (Hyderabad) | **`ap-south-2`** | **arn:aws:lambda:ap-south-2:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:73**{: .copyMe}:clipboard: |
+ | Asia Pacific (Singapore) | **`ap-southeast-1`** | **arn:aws:lambda:ap-southeast-1:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:73**{: .copyMe}:clipboard: |
+ | Asia Pacific (Sydney) | **`ap-southeast-2`** | **arn:aws:lambda:ap-southeast-2:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:73**{: .copyMe}:clipboard: |
+ | Asia Pacific (Jakarta) | **`ap-southeast-3`** | **arn:aws:lambda:ap-southeast-3:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:73**{: .copyMe}:clipboard: |
+ | Asia Pacific (Melbourne) | **`ap-southeast-4`** | **arn:aws:lambda:ap-southeast-4:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:73**{: .copyMe}:clipboard: |
+ | Canada (Central) | **`ca-central-1`** | **arn:aws:lambda:ca-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:73**{: .copyMe}:clipboard: |
+ | Canada (West) | **`ca-west-1`** | **arn:aws:lambda:ca-west-1:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:73**{: .copyMe}:clipboard: |
+ | Europe (Frankfurt) | **`eu-central-1`** | **arn:aws:lambda:eu-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:73**{: .copyMe}:clipboard: |
+ | Europe (Zurich) | **`eu-central-2`** | **arn:aws:lambda:eu-central-2:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:73**{: .copyMe}:clipboard: |
+ | Europe (Stockholm) | **`eu-north-1`** | **arn:aws:lambda:eu-north-1:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:73**{: .copyMe}:clipboard: |
+ | Europe (Milan) | **`eu-south-1`** | **arn:aws:lambda:eu-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:73**{: .copyMe}:clipboard: |
+ | Europe (Spain) | **`eu-south-2`** | **arn:aws:lambda:eu-south-2:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:73**{: .copyMe}:clipboard: |
+ | Europe (Ireland) | **`eu-west-1`** | **arn:aws:lambda:eu-west-1:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:73**{: .copyMe}:clipboard: |
+ | Europe (London) | **`eu-west-2`** | **arn:aws:lambda:eu-west-2:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:73**{: .copyMe}:clipboard: |
+ | Europe (Paris) | **`eu-west-3`** | **arn:aws:lambda:eu-west-3:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:73**{: .copyMe}:clipboard: |
+ | Middle East (Israel) | **`il-central-1`** | **arn:aws:lambda:il-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:73**{: .copyMe}:clipboard: |
+ | Middle East (UAE) | **`me-central-1`** | **arn:aws:lambda:me-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:73**{: .copyMe}:clipboard: |
+ | Middle East (Bahrain) | **`me-south-1`** | **arn:aws:lambda:me-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:73**{: .copyMe}:clipboard: |
+ | South America (São Paulo) | **`sa-east-1`** | **arn:aws:lambda:sa-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:73**{: .copyMe}:clipboard: |
+ | US East (N. Virginia) | **`us-east-1`** | **arn:aws:lambda:us-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:73**{: .copyMe}:clipboard: |
+ | US East (Ohio) | **`us-east-2`** | **arn:aws:lambda:us-east-2:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:73**{: .copyMe}:clipboard: |
+ | US West (N. California) | **`us-west-1`** | **arn:aws:lambda:us-west-1:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:73**{: .copyMe}:clipboard: |
+ | US West (Oregon) | **`us-west-2`** | **arn:aws:lambda:us-west-2:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:73**{: .copyMe}:clipboard: |
+
+
+ === "Infrastructure as Code (IaC)"
+
+ > Are we missing a framework? please create [a documentation request](https://github.com/aws-powertools/powertools-lambda-python/issues/new?assignees=&labels=documentation%2Ctriage&projects=&template=documentation_improvements.yml&title=Docs%3A+TITLE){target="_blank" rel="nofollow"}.
+
+ Thanks to the community, we've covered most popular frameworks on how to add a Lambda Layer to an existing function.
=== "x86_64"
@@ -148,117 +232,36 @@ You can install Powertools for AWS Lambda (Python) using your favorite dependenc
--8<-- "examples/homepage/install/arm64/amplify.txt"
```
-### Local development
-
-!!! info "Using Lambda Layer? Simply add [**`"aws-lambda-powertools[all]"`**](#){: .copyMe}:clipboard: as a development dependency."
+ === "Inspect Lambda Layer contents"
-Powertools for AWS Lambda (Python) relies on the [AWS SDK bundled in the Lambda runtime](https://docs.aws.amazon.com/lambda/latest/dg/lambda-python.html){target="_blank"}. This helps us achieve an optimal package size and initialization. However, when developing locally, you need to install AWS SDK as a development dependency to support IDE auto-completion and to run your tests locally:
-
-- __Pip__: [**`pip install "aws-lambda-powertools[aws-sdk]"`**](#){: .copyMe}:clipboard:
-- __Poetry__: [**`poetry add "aws-lambda-powertools[aws-sdk]" --group dev`**](#){: .copyMe}:clipboard:
-- __Pdm__: [**`pdm add -dG "aws-lambda-powertools[aws-sdk]"`**](#){: .copyMe}:clipboard:
+ You can use AWS CLI to generate a pre-signed URL to download the contents of our Lambda Layer.
-__A word about dependency resolution__
+ ```bash title="AWS CLI command to download Lambda Layer content"
+ aws lambda get-layer-version-by-arn --arn arn:aws:lambda:eu-west-1:017000801446:layer:AWSLambdaPowertoolsPythonV2:73 --region eu-west-1
+ ```
-In this context, `[aws-sdk]` is an alias to the `boto3` package. Due to dependency resolution, it'll either install:
+ You'll find the pre-signed URL under `Location` key as part of the CLI command output.
-- __(A)__ the SDK version available in [Lambda runtime](https://docs.aws.amazon.com/lambda/latest/dg/lambda-python.html){target="_blank"}
-- __(B)__ a more up-to-date version if another package you use also depends on `boto3`, for example [Powertools for AWS Lambda (Python) Tracer](core/tracer.md){target="_blank"}
+=== "Serverless Application Repository (SAR)"
-### Lambda Layer
-
-[Lambda Layer](https://docs.aws.amazon.com/lambda/latest/dg/configuration-layers.html){target="_blank"} is a .zip file archive that can contain additional code, pre-packaged dependencies, data, or configuration files. We compile and optimize [all dependencies](#install), and remove duplicate dependencies [already available in the Lambda runtime](https://github.com/aws-powertools/powertools-lambda-layer-cdk/blob/d24716744f7d1f37617b4998c992c4c067e19e64/layer/Python/Dockerfile#L36){target="_blank"} to achieve the most optimal size.
-
-??? note "Click to expand and copy any regional Lambda Layer ARN"
-
- === "x86_64"
-
- | Region | Layer ARN |
- | -------------------- | --------------------------------------------------------------------------------------------------------- |
- | **`af-south-1`** | **arn:aws:lambda:af-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV2:72**{: .copyMe}:clipboard: |
- | **`ap-east-1`** | **arn:aws:lambda:ap-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV2:72**{: .copyMe}:clipboard: |
- | **`ap-northeast-1`** | **arn:aws:lambda:ap-northeast-1:017000801446:layer:AWSLambdaPowertoolsPythonV2:72**{: .copyMe}:clipboard: |
- | **`ap-northeast-2`** | **arn:aws:lambda:ap-northeast-2:017000801446:layer:AWSLambdaPowertoolsPythonV2:72**{: .copyMe}:clipboard: |
- | **`ap-northeast-3`** | **arn:aws:lambda:ap-northeast-3:017000801446:layer:AWSLambdaPowertoolsPythonV2:72**{: .copyMe}:clipboard: |
- | **`ap-south-1`** | **arn:aws:lambda:ap-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV2:72**{: .copyMe}:clipboard: |
- | **`ap-south-2`** | **arn:aws:lambda:ap-south-2:017000801446:layer:AWSLambdaPowertoolsPythonV2:72**{: .copyMe}:clipboard: |
- | **`ap-southeast-1`** | **arn:aws:lambda:ap-southeast-1:017000801446:layer:AWSLambdaPowertoolsPythonV2:72**{: .copyMe}:clipboard: |
- | **`ap-southeast-2`** | **arn:aws:lambda:ap-southeast-2:017000801446:layer:AWSLambdaPowertoolsPythonV2:72**{: .copyMe}:clipboard: |
- | **`ap-southeast-3`** | **arn:aws:lambda:ap-southeast-3:017000801446:layer:AWSLambdaPowertoolsPythonV2:72**{: .copyMe}:clipboard: |
- | **`ap-southeast-4`** | **arn:aws:lambda:ap-southeast-4:017000801446:layer:AWSLambdaPowertoolsPythonV2:72**{: .copyMe}:clipboard: |
- | **`ca-central-1`** | **arn:aws:lambda:ca-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV2:72**{: .copyMe}:clipboard: |
- | **`ca-west-1`** | **arn:aws:lambda:ca-west-1:017000801446:layer:AWSLambdaPowertoolsPythonV2:72**{: .copyMe}:clipboard: |
- | **`eu-central-1`** | **arn:aws:lambda:eu-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV2:72**{: .copyMe}:clipboard: |
- | **`eu-central-2`** | **arn:aws:lambda:eu-central-2:017000801446:layer:AWSLambdaPowertoolsPythonV2:72**{: .copyMe}:clipboard: |
- | **`eu-north-1`** | **arn:aws:lambda:eu-north-1:017000801446:layer:AWSLambdaPowertoolsPythonV2:72**{: .copyMe}:clipboard: |
- | **`eu-south-1`** | **arn:aws:lambda:eu-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV2:72**{: .copyMe}:clipboard: |
- | **`eu-south-2`** | **arn:aws:lambda:eu-south-2:017000801446:layer:AWSLambdaPowertoolsPythonV2:72**{: .copyMe}:clipboard: |
- | **`eu-west-1`** | **arn:aws:lambda:eu-west-1:017000801446:layer:AWSLambdaPowertoolsPythonV2:72**{: .copyMe}:clipboard: |
- | **`eu-west-2`** | **arn:aws:lambda:eu-west-2:017000801446:layer:AWSLambdaPowertoolsPythonV2:72**{: .copyMe}:clipboard: |
- | **`eu-west-3`** | **arn:aws:lambda:eu-west-3:017000801446:layer:AWSLambdaPowertoolsPythonV2:72**{: .copyMe}:clipboard: |
- | **`il-central-1`** | **arn:aws:lambda:il-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV2:72**{: .copyMe}:clipboard: |
- | **`me-central-1`** | **arn:aws:lambda:me-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV2:72**{: .copyMe}:clipboard: |
- | **`me-south-1`** | **arn:aws:lambda:me-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV2:72**{: .copyMe}:clipboard: |
- | **`sa-east-1`** | **arn:aws:lambda:sa-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV2:72**{: .copyMe}:clipboard: |
- | **`us-east-1`** | **arn:aws:lambda:us-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV2:72**{: .copyMe}:clipboard: |
- | **`us-east-2`** | **arn:aws:lambda:us-east-2:017000801446:layer:AWSLambdaPowertoolsPythonV2:72**{: .copyMe}:clipboard: |
- | **`us-west-1`** | **arn:aws:lambda:us-west-1:017000801446:layer:AWSLambdaPowertoolsPythonV2:72**{: .copyMe}:clipboard: |
- | **`us-west-2`** | **arn:aws:lambda:us-west-2:017000801446:layer:AWSLambdaPowertoolsPythonV2:72**{: .copyMe}:clipboard: |
-
- === "arm64"
-
- | Region | Layer ARN |
- | -------------------- | --------------------------------------------------------------------------------------------------------------- |
- | **`af-south-1`** | **arn:aws:lambda:af-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:72**{: .copyMe}:clipboard: |
- | **`ap-east-1`** | **arn:aws:lambda:ap-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:72**{: .copyMe}:clipboard: |
- | **`ap-northeast-1`** | **arn:aws:lambda:ap-northeast-1:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:72**{: .copyMe}:clipboard: |
- | **`ap-northeast-2`** | **arn:aws:lambda:ap-northeast-2:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:72**{: .copyMe}:clipboard: |
- | **`ap-northeast-3`** | **arn:aws:lambda:ap-northeast-3:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:72**{: .copyMe}:clipboard: |
- | **`ap-south-1`** | **arn:aws:lambda:ap-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:72**{: .copyMe}:clipboard: |
- | **`ap-south-2`** | **arn:aws:lambda:ap-south-2:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:72**{: .copyMe}:clipboard: |
- | **`ap-southeast-1`** | **arn:aws:lambda:ap-southeast-1:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:72**{: .copyMe}:clipboard: |
- | **`ap-southeast-2`** | **arn:aws:lambda:ap-southeast-2:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:72**{: .copyMe}:clipboard: |
- | **`ap-southeast-3`** | **arn:aws:lambda:ap-southeast-3:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:72**{: .copyMe}:clipboard: |
- | **`ca-central-1`** | **arn:aws:lambda:ca-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:72**{: .copyMe}:clipboard: |
- | **`eu-central-1`** | **arn:aws:lambda:eu-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:72**{: .copyMe}:clipboard: |
- | **`eu-central-2`** | **arn:aws:lambda:eu-central-2:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:72**{: .copyMe}:clipboard: |
- | **`eu-north-1`** | **arn:aws:lambda:eu-north-1:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:72**{: .copyMe}:clipboard: |
- | **`eu-south-1`** | **arn:aws:lambda:eu-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:72**{: .copyMe}:clipboard: |
- | **`eu-south-2`** | **arn:aws:lambda:eu-south-2:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:72**{: .copyMe}:clipboard: |
- | **`eu-west-1`** | **arn:aws:lambda:eu-west-1:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:72**{: .copyMe}:clipboard: |
- | **`eu-west-2`** | **arn:aws:lambda:eu-west-2:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:72**{: .copyMe}:clipboard: |
- | **`eu-west-3`** | **arn:aws:lambda:eu-west-3:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:72**{: .copyMe}:clipboard: |
- | **`il-central-1`** | **arn:aws:lambda:il-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:72**{: .copyMe}:clipboard: |
- | **`me-central-1`** | **arn:aws:lambda:me-central-1:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:72**{: .copyMe}:clipboard: |
- | **`me-south-1`** | **arn:aws:lambda:me-south-1:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:72**{: .copyMe}:clipboard: |
- | **`sa-east-1`** | **arn:aws:lambda:sa-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:72**{: .copyMe}:clipboard: |
- | **`us-east-1`** | **arn:aws:lambda:us-east-1:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:72**{: .copyMe}:clipboard: |
- | **`us-east-2`** | **arn:aws:lambda:us-east-2:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:72**{: .copyMe}:clipboard: |
- | **`us-west-1`** | **arn:aws:lambda:us-west-1:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:72**{: .copyMe}:clipboard: |
- | **`us-west-2`** | **arn:aws:lambda:us-west-2:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:72**{: .copyMe}:clipboard: |
-
-**Want to inspect the contents of the Layer?**
-
-The pre-signed URL to download this Lambda Layer will be within `Location` key in the CLI output. The CLI output will also contain the Powertools for AWS Lambda version it contains.
-
-```bash title="AWS CLI command to download Lambda Layer content"
-aws lambda get-layer-version-by-arn --arn arn:aws:lambda:eu-west-1:017000801446:layer:AWSLambdaPowertoolsPythonV2:72 --region eu-west-1
-```
+ We provide a SAR App that deploys a CloudFormation stack with a copy of our Lambda Layer in your AWS account and region.
-#### SAR
+ Compared with the [public Layer ARN](#lambda-layer) option, the advantage is being able to use a semantic version.
-Serverless Application Repository (SAR) App deploys a CloudFormation stack with a copy of our Lambda Layer in your AWS account and region.
+ | App | | | ARN |
+ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --- | --- | ----------------------------------------------------------------------------------------------------------------------------- |
+ | [**aws-lambda-powertools-python-layer**](https://serverlessrepo.aws.amazon.com/applications/eu-west-1/057560766410/aws-lambda-powertools-python-layer){target="_blank"} | | | __arn:aws:serverlessrepo:eu-west-1:057560766410:applications/aws-lambda-powertools-python-layer__{: .copyMe}:clipboard: |
+ | [**aws-lambda-powertools-python-layer-arm64**](https://serverlessrepo.aws.amazon.com/applications/eu-west-1/057560766410/aws-lambda-powertools-python-layer-arm64){target="_blank"} | | | __arn:aws:serverlessrepo:eu-west-1:057560766410:applications/aws-lambda-powertools-python-layer-arm64__{: .copyMe}:clipboard: |
-Compared with the [public Layer ARN](#lambda-layer) option, SAR allows you to choose a semantic version and deploys a Layer in your target account.
+ ??? question "Don't have enough permissions? Expand for a least-privilege IAM policy example"
-| App | ARN | Description |
-| ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------- |
-| [**aws-lambda-powertools-python-layer**](https://serverlessrepo.aws.amazon.com/applications/eu-west-1/057560766410/aws-lambda-powertools-python-layer){target="_blank"} | [arn:aws:serverlessrepo:eu-west-1:057560766410:applications/aws-lambda-powertools-python-layer](#){: .copyMe}:clipboard: | Contains all extra dependencies (e.g: pydantic). |
-| [**aws-lambda-powertools-python-layer-arm64**](https://serverlessrepo.aws.amazon.com/applications/eu-west-1/057560766410/aws-lambda-powertools-python-layer-arm64){target="_blank"} | [arn:aws:serverlessrepo:eu-west-1:057560766410:applications/aws-lambda-powertools-python-layer-arm64](#){: .copyMe}:clipboard: | Contains all extra dependencies (e.g: pydantic). For arm64 functions. |
+ Credits to [mwarkentin](https://github.com/mwarkentin){target="_blank" rel="nofollow"} for providing the scoped down IAM permissions.
-??? note "Click to expand and copy SAR code snippets for popular frameworks"
+ ```yaml hl_lines="21-52" title="Least-privileged IAM permissions SAM example"
+ --8<-- "examples/homepage/install/sar/scoped_down_iam.yaml"
+ ```
- You can create a shared Lambda Layers stack and make this along with other account level layers stack.
+ If you're using Infrastructure as Code, here are some excerpts on how to use SAR:
=== "SAM"
@@ -280,17 +283,38 @@ Compared with the [public Layer ARN](#lambda-layer) option, SAR allows you to ch
=== "Terraform"
- > Credits to [Dani Comnea](https://github.com/DanyC97){target="_blank" rel="nofollow"} for providing the Terraform equivalent.
+ > Credits to [Dani Comnea](https://github.com/DanyC97){target="_blank" rel="nofollow"} for providing the Terraform equivalent.
```terraform hl_lines="12-13 15-20 23-25 40"
--8<-- "examples/homepage/install/sar/terraform.tf"
```
- Credits to [mwarkentin](https://github.com/mwarkentin){target="_blank" rel="nofollow"} for providing the scoped down IAM permissions below.
+=== "Alpha releases"
+
+ Every morning during business days _(~8am UTC)_, we publish a `prerelease` to PyPi to accelerate customer feedback on **unstable** releases / bugfixes until they become production ready.
- ```yaml hl_lines="21-52" title="Least-privileged IAM permissions SAM example"
- --8<-- "examples/homepage/install/sar/scoped_down_iam.yaml"
- ```
+ Here's how you can use them:
+
+ - __Pip__: [**`pip install --pre "aws-lambda-powertools"`**](#){: .copyMe}:clipboard:
+ - __Poetry__: [**`poetry add --allow-prereleases "aws-lambda-powertools" --group dev`**](#){: .copyMe}:clipboard:
+ - __Pdm__: [**`pdm add -dG --prerelease "aws-lambda-powertools"`**](#){: .copyMe}:clipboard:
+
+### Local development
+
+!!! info "Using Lambda Layer? Simply add [**`"aws-lambda-powertools[all]"`**](#){: .copyMe}:clipboard: as a development dependency."
+
+Powertools for AWS Lambda (Python) relies on the [AWS SDK bundled in the Lambda runtime](https://docs.aws.amazon.com/lambda/latest/dg/lambda-python.html){target="_blank"}. This helps us achieve an optimal package size and initialization. However, when developing locally, you need to install AWS SDK as a development dependency to support IDE auto-completion and to run your tests locally:
+
+- __Pip__: [**`pip install "aws-lambda-powertools[aws-sdk]"`**](#){: .copyMe}:clipboard:
+- __Poetry__: [**`poetry add "aws-lambda-powertools[aws-sdk]" --group dev`**](#){: .copyMe}:clipboard:
+- __Pdm__: [**`pdm add -dG "aws-lambda-powertools[aws-sdk]"`**](#){: .copyMe}:clipboard:
+
+__A word about dependency resolution__
+
+In this context, `[aws-sdk]` is an alias to the `boto3` package. Due to dependency resolution, it'll either install:
+
+- __(A)__ the SDK version available in [Lambda runtime](https://docs.aws.amazon.com/lambda/latest/dg/lambda-python.html){target="_blank"}
+- __(B)__ a more up-to-date version if another package you use also depends on `boto3`, for example [Powertools for AWS Lambda (Python) Tracer](core/tracer.md){target="_blank"}
## Quick getting started
@@ -374,7 +398,7 @@ There are many ways you can help us gain future investments to improve everyone'
Add your company name and logo on our [landing page](https://powertools.aws.dev).
- [:octicons-arrow-right-24: GitHub Issue template]((https://github.com/aws-powertools/powertools-lambda-python/issues/new?assignees=&labels=customer-reference&template=support_powertools.yml&title=%5BSupport+Lambda+Powertools%5D%3A+%3Cyour+organization+name%3E){target="_blank"})
+ [:octicons-arrow-right-24: GitHub Issue template](https://github.com/aws-powertools/powertools-lambda-python/issues/new?assignees=&labels=customer-reference&template=support_powertools.yml&title=%5BSupport+Lambda+Powertools%5D%3A+%3Cyour+organization+name%3E){target="_blank"}
- :mega:{ .lg .middle } __Share your work__
@@ -400,6 +424,9 @@ Knowing which companies are using this library is important to help prioritize t
+[**Brsk**](https://www.brsk.co.uk/){target="_blank" rel="nofollow"}
+{ .card }
+
[**Capital One**](https://www.capitalone.com/){target="_blank" rel="nofollow"}
{ .card }
@@ -448,8 +475,12 @@ Knowing which companies are using this library is important to help prioritize t
!!! note "Layers help us understand who uses Powertools for AWS Lambda (Python) in a non-intrusive way."
+
+
When [using Layers](#lambda-layer), you can add Powertools for AWS Lambda (Python) as a dev dependency to not impact the development process. For Layers, we pre-package all dependencies, compile and optimize for storage and both x86 and ARM architecture.
+
+
## Tenets
These are our core principles to guide our decision making.
diff --git a/docs/maintainers.md b/docs/maintainers.md
index 4fd4f109a33..c2d811e0a8a 100644
--- a/docs/maintainers.md
+++ b/docs/maintainers.md
@@ -154,9 +154,9 @@ Firstly, make sure the commit history in the `develop` branch **(1)** it's up to
**Looks good, what's next?**
-Kickoff the `Release` workflow with the intended version - this might take around 25m-30m to complete.
+Kickoff the [`Release` workflow](https://github.com/aws-powertools/powertools-lambda-python/blob/6db9079d21698b72f5d36d72c993c1aad7276db6/.github/workflows/release.yml#L3) with the intended version - this might take around 25m-30m to complete.
-Once complete, you can start drafting the release notes to let customers know **what changed and what's in it for them (a.k.a why they should care)**. We have guidelines in the release notes section so you know what good looks like.
+Once complete, you can start drafting the release notes to let customers know **what changed and what's in it for them (a.k.a why they should care)**. We have guidelines in the [release notes section](#drafting-release-notes) so you know what good looks like.
> **NOTE**: Documentation might take a few minutes to reflect the latest version due to caching and CDN invalidations.
@@ -231,33 +231,43 @@ Release complete : milestone, m6, 10:31,2m
#### Drafting release notes
+!!! info "Make sure the release workflow completed before you edit release notes."
+
Visit the [Releases page](https://github.com/aws-powertools/powertools-lambda-python/releases) and choose the edit pencil button.
Make sure the `tag` field reflects the new version you're releasing, the target branch field is set to `develop`, and `release title` matches your tag e.g., `v1.26.0`.
You'll notice we group all changes based on their [labels](#labels) like `feature`, `bug`, `documentation`, etc.
+!!! question inline end "Spotted a typo?"
-**I spotted a typo or incorrect grouping - how do I fix it?**
+ Edit the respective PR title/labels and run the [Release Drafter workflow](https://github.com/aws-powertools/powertools-lambda-python/actions/workflows/release-drafter.yml).
-Edit the respective PR title and update their [labels](#labels). Then run the [Release Drafter workflow](https://github.com/aws-powertools/powertools-lambda-python/actions/workflows/release-drafter.yml) to update the Draft release.
+!!! question "All good, what's next?"
-> **NOTE**: This won't change the CHANGELOG as the merge commit is immutable. Don't worry about it. We'd only rewrite git history only if this can lead to confusion and we'd pair with another maintainer.
+The best part comes now!
-**All looking good, what's next?**
+Replace the placeholder `[Human readable summary of changes]` with what you'd like to communicate to customers what this release is all about.
-The best part comes now. Replace the placeholder `[Human readable summary of changes]` with what you'd like to communicate to customers what this release is all about. Rule of thumb: always put yourself in the customers shoes.
+!!! tip inline end "Always put yourself in the customers shoes. Most read the first sentence only to know whether this is for them."
These are some questions to keep in mind when drafting your first or future release notes:
-- Can customers understand at a high level what changed in this release?
-- Is there a link to the documentation where they can read more about each main change?
-- Are there any graphics or [code snippets](https://carbon.now.sh/) that can enhance readability?
-- Are we calling out any key contributor(s) to this release?
- - All contributors are automatically credited, use this as an exceptional case to feature them
+- **Can customers briefly understand the main changes in less than 30s?**
+ - _tip: first paragraph is punchy and optimizes for dependabot-like notifications._
+- **Are we calling out key contributor(s) to this release?**
+- **Is it clear what each change enables/unlocks and before?**
+ - _tip: use present and active voice; lead with the answer._
+- **Does it include a link to the documentation for each main change?**
+ - _tip: release explains what a change unblocks/enables (before/after), docs go in details_
+- **Is code snippet better in text or [graphic](https://carbon.now.sh)?**
+- **Does code snippet focus on the change only?**
+ - _tip: release snippets highlight functionality, no need to be functional (that's docs)_
Once you're happy, hit `Publish release` 🎉🎉🎉.
-This will kick off the [Publishing workflow](https://github.com/aws-powertools/powertools-lambda-python/actions/workflows/release.yml) and within a few minutes you should see the latest version in PyPi, and all issues labeled as `pending-release` will be closed and notified.
+### Releasing an alpha release
+
+We publish alpha releases _(`prerelease`)_ every morning during business days (~8am UTC). You can also manually trigger `pre-release` workflow when needed.
### Run end to end tests
diff --git a/docs/utilities/validation.md b/docs/utilities/validation.md
index 1b569ddc14c..51085d417fa 100644
--- a/docs/utilities/validation.md
+++ b/docs/utilities/validation.md
@@ -147,10 +147,10 @@ Here is a handy table with built-in envelopes along with their JMESPath expressi
| **`API_GATEWAY_HTTP`** | `powertools_json(body)` |
| **`API_GATEWAY_REST`** | `powertools_json(body)` |
| **`CLOUDWATCH_EVENTS_SCHEDULED`** | `detail` |
-| **`CLOUDWATCH_LOGS`** | `awslogs.powertools_base64_gzip(data) | powertools_json(@).logEvents[*]` |
+| **`CLOUDWATCH_LOGS`** | `awslogs.powertools_base64_gzip(data)` or `powertools_json(@).logEvents[*]` |
| **`EVENTBRIDGE`** | `detail` |
| **`KINESIS_DATA_STREAM`** | `Records[*].kinesis.powertools_json(powertools_base64(data))` |
-| **`SNS`** | `Records[0].Sns.Message | powertools_json(@)` |
+| **`SNS`** | `Records[0].Sns.Message` or `powertools_json(@)` |
| **`SQS`** | `Records[*].powertools_json(body)` |
## Advanced
@@ -199,3 +199,33 @@ You can use our built-in [JMESPath functions](./jmespath_functions.md){target="_
???+ info
We use these for [built-in envelopes](#built-in-envelopes) to easily to decode and unwrap events from sources like Kinesis, CloudWatch Logs, etc.
+
+### Validating with external references
+
+JSON Schema [allows schemas to reference other schemas](https://json-schema.org/understanding-json-schema/structuring#dollarref) using the `$ref` keyword with a URI value. By default, `fastjsonschema` will make a HTTP request to resolve this URI.
+
+You can use `handlers` parameter to have full control over how references schemas are fetched. This is useful when you might want to optimize caching, reducing HTTP calls, or fetching them from non-HTTP endpoints.
+
+=== "custom_handlers.py"
+
+ ```python hl_lines="1 7 8 11"
+ --8<-- "examples/validation/src/custom_handlers.py"
+ ```
+
+=== "custom_handlers_parent_schema"
+
+ ```python hl_lines="1 7"
+ --8<-- "examples/validation/src/custom_handlers_schema.py"
+ ```
+
+=== "custom_handlers_child_schema"
+
+ ```python hl_lines="12"
+ --8<-- "examples/validation/src/custom_handlers_schema.py"
+ ```
+
+=== "custom_handlers_payload.json"
+
+ ```json hl_lines="2"
+ --8<-- "examples/validation/src/custom_handlers_payload.json"
+ ```
diff --git a/docs/we_made_this.md b/docs/we_made_this.md
index efa29478471..c3c2167e874 100644
--- a/docs/we_made_this.md
+++ b/docs/we_made_this.md
@@ -17,11 +17,11 @@ Join us on [Discord](https://discord.gg/B8zZKbbyET){target="_blank" rel="nofollo
## Blog posts
-### AWS Lambda Cookbook — Following best practices with Lambda Powertools
+### AWS Lambda Cookbook — Following best practices with Powertools for AWS Lambda
> **Author: [Ran Isenberg](mailto:ran.isenberg@ranthebuilder.cloud) [:material-twitter:](https://twitter.com/IsenbergRan){target="_blank" rel="nofollow"} [:material-linkedin:](https://www.linkedin.com/in/ranisenberg/){target="_blank" rel="nofollow"}**
-A collection of articles explaining in detail how Lambda Powertools helps with a Serverless adoption strategy and its challenges.
+A collection of articles explaining in detail how Powertools for AWS Lambda helps with a Serverless adoption strategy and its challenges.
* [Part 1 - Logging](https://www.ranthebuilder.cloud/post/aws-lambda-cookbook-elevate-your-handler-s-code-part-1-logging){:target="_blank"}
@@ -35,12 +35,14 @@ A collection of articles explaining in detail how Lambda Powertools helps with a
* [Part 6 - Configuration & Feature Flags](https://www.ranthebuilder.cloud/post/aws-lambda-cookbook-part-6-feature-flags-configuration-best-practices){:target="_blank"}
-* [Serverless API Idempotency with AWS Lambda Powertools and CDK](https://www.ranthebuilder.cloud/post/serverless-api-idempotency-with-aws-lambda-powertools-and-cdk){:target="_blank"}
+* [Serverless API Idempotency with AWS Powertools for AWS Lambda and CDK](https://www.ranthebuilder.cloud/post/serverless-api-idempotency-with-aws-lambda-powertools-and-cdk){:target="_blank"}
* [Effective Amazon SQS Batch Handling with Powertools for AWS Lambda (Python)](https://www.ranthebuilder.cloud/post/effective-amazon-sqs-batch-handling-with-aws-lambda-powertools){:target="_blank"}
* [Serverless API Documentation with Powertools for AWS](https://www.ranthebuilder.cloud/post/serverless-open-api-documentation-with-aws-powertools){:target="_blank"}
+* [Best practices for accelerating development with serverless blueprints](https://aws.amazon.com/blogs/infrastructure-and-automation/best-practices-for-accelerating-development-with-serverless-blueprints/){:target="_blank"}
+
### Making all your APIs idempotent
> **Author: [Michael Walmsley](https://twitter.com/walmsles){target="_blank" rel="nofollow"}** :material-twitter:
@@ -49,7 +51,7 @@ This article dives into what idempotency means for APIs, their use cases, and ho
* [blog.walmsles.io/making-all-your-apis-idempotent](https://blog.walmsles.io/making-all-your-apis-idempotent){target="_blank" rel="nofollow"}
-### Deep dive on Lambda Powertools Idempotency feature
+### Deep dive on Powertools for AWS Lambda Idempotency feature
> **Author: [Michael Walmsley](https://twitter.com/walmsles){target="_blank" rel="nofollow"}** :material-twitter:
@@ -57,7 +59,7 @@ This article describes how to best calculate your idempotency token, implementat
* [blog.walmsles.io/aws-lambda-powertools-idempotency-a-deeper-dive](https://blog.walmsles.io/aws-lambda-powertools-idempotency-a-deeper-dive){target="_blank" rel="nofollow"}
-### Developing AWS Lambda functions with AWS Lambda Powertools
+### Developing AWS Lambda functions with Powertools for AWS Lambda
> **Author: [Stephan Huber](https://linkedin.com/in/sthuber90){target="_blank" rel="nofollow"}** :material-linkedin:
@@ -74,7 +76,7 @@ This article walks through a sample AWS EventBridge cookiecutter template presen
* [binx.io/2022/10/11/speedup-event-driven-projects/](https://binx.io/2022/10/11/speedup-event-driven-projects/){target="_blank" rel="nofollow"}
* [Slides](https://www.slideshare.net/JorisConijn/let-codecommit-work-for-you){target="_blank" rel="nofollow"}
-### Implementing Feature Flags with AWS AppConfig and AWS Lambda Powertools
+### Implementing Feature Flags with AWS AppConfig and Powertools for AWS Lambda
> **Author: [Ran Isenberg](mailto:ran.isenberg@ranthebuilder.cloud) [:material-twitter:](https://twitter.com/IsenbergRan){target="_blank" rel="nofollow"} [:material-linkedin:](https://www.linkedin.com/in/ranisenberg/){target="_blank" rel="nofollow"}**
@@ -128,13 +130,13 @@ This article will walk you through using Powertools for AWS Lambda to optimize y
> **Author: [Ran Isenberg](mailto:ran.isenberg@ranthebuilder.cloud) [:material-twitter:](https://twitter.com/IsenbergRan){target="_blank" rel="nofollow"} [:material-linkedin:](https://www.linkedin.com/in/ranisenberg/){target="_blank" rel="nofollow"}**
-When building applications with AWS Lambda it is critical to verify the data structure and validate the input due to the multiple different sources that can trigger them. In this session Ran Isenberg (CyberArk) will present one of the interesting features of AWS Lambda Powertools for python: the parser.
+When building applications with AWS Lambda it is critical to verify the data structure and validate the input due to the multiple different sources that can trigger them. In this session Ran Isenberg (CyberArk) will present one of the interesting features of Powertools for AWS Lambda for python: the parser.
In this session you will learn how to increase code quality, extensibility and testability, boost you productivity and ship rock solid apps to production.
-#### Talk DEV to me | Feature Flags with AWS Lambda Powertools
+#### Talk DEV to me | Feature Flags with Powertools for AWS Lambda
> **Author: [Ran Isenberg](mailto:ran.isenberg@ranthebuilder.cloud) [:material-twitter:](https://twitter.com/IsenbergRan){target="_blank" rel="nofollow"} [:material-linkedin:](https://www.linkedin.com/in/ranisenberg/){target="_blank" rel="nofollow"}**
@@ -148,7 +150,7 @@ A deep dive in the [Feature Flags](./utilities/feature_flags.md){target="_blank"
Feature flags can improve your CI/CD process by enabling capabilities otherwise not possible, thus making them an enabler of DevOps and a crucial part of continuous integration. Partial rollouts, A/B testing, and the ability to quickly change a configuration without redeploying code are advantages you gain by using features flags.
-In this talk, you will learn the added value of using feature flags as part of your CI/CD process and how AWS Lambda Powertools can help with that.
+In this talk, you will learn the added value of using feature flags as part of your CI/CD process and how Powertools for AWS Lambda can help with that.
#### AWS re:invent 2023 - OPN305 - The Pragmatic Serverless Python Developer
@@ -164,13 +166,13 @@ Join to discover tools and patterns for effective serverless development with Py
## Workshops
-### Introduction to Lambda Powertools
+### Introduction to Powertools for AWS Lambda
> **Author: [Michael Walmsley](https://twitter.com/walmsles){target="_blank" rel="nofollow"}** :material-twitter:
This repo contains documentation for a live coding workshop for the AWS Programming and Tools Meetup in Melbourne. The workshop will start with the SAM Cli "Hello World" example API project.
-Throughout the labs we will introduce each of the AWS Lambda Powertools Core utilities to showcase how simple they are to use and adopt for all your projects, and how powerful they are at bringing you closer to the Well Architected Serverless Lens.
+Throughout the labs we will introduce each of the Powertools for AWS Lambda Core utilities to showcase how simple they are to use and adopt for all your projects, and how powerful they are at bringing you closer to the Well Architected Serverless Lens.
* :material-github: [github.com/walmsles/lambda-powertools-coding-workshop](https://github.com/walmsles/lambda-powertools-coding-workshop){target="_blank" rel="nofollow"}
@@ -186,7 +188,7 @@ Throughout the labs we will introduce each of the AWS Lambda Powertools Core uti
This repository provides a working, deployable, open source based, AWS Lambda handler and [AWS CDK](https://aws.amazon.com/cdk/){target="_blank" rel="nofollow"} Python code.
-This handler embodies Serverless best practices and has all the bells and whistles for a proper production ready handler. It uses many of the AWS Lambda Powertools utilities for Python.
+This handler embodies Serverless best practices and has all the bells and whistles for a proper production ready handler. It uses many of the Powertools for AWS Lambda utilities for Python.
:material-github: [github.com/ran-isenberg/aws-lambda-handler-cookbook](https://github.com/ran-isenberg/aws-lambda-handler-cookbook){:target="_blank"}
@@ -203,8 +205,8 @@ session: OPN305 - The pragmatic serverless python developer.
> **Author: [Santiago Garcia Arango](mailto:san99tiago@gmail.com) [:material-web:](https://san99tiago.com/){target="_blank" rel="nofollow"} [:material-linkedin:](https://www.linkedin.com/in/san99tiago/){target="_blank" rel="nofollow"}**
-This repository contains a well documented example of a Transactional Messages App that illustrates how to use Lambda PowerTools to process SQS messages in batches (with IaC on top of CDK).
+This repository contains a well documented example of a Transactional Messages App that illustrates how to use Powertools for AWS Lambda to process SQS messages in batches (with IaC on top of CDK).
-It uses LambdaPowerTools Logger, Tracing, DataClasses and includes unit tests.
+It uses Powertools for AWS Lambda Logger, Tracing, DataClasses and includes unit tests.
:material-github: [github.com/san99tiago/aws-cdk-transactional-messages](https://github.com/san99tiago/aws-cdk-transactional-messages){:target="_blank"}
diff --git a/examples/event_handler_graphql/sam/template.yaml b/examples/event_handler_graphql/sam/template.yaml
index bc4faa34319..1c75d18ae55 100644
--- a/examples/event_handler_graphql/sam/template.yaml
+++ b/examples/event_handler_graphql/sam/template.yaml
@@ -5,7 +5,7 @@ Description: Hello world Direct Lambda Resolver
Globals:
Function:
Timeout: 5
- Runtime: python3.9
+ Runtime: python3.12
Tracing: Active
Environment:
Variables:
diff --git a/examples/event_handler_graphql/src/advanced_batch_async_resolver.py b/examples/event_handler_graphql/src/advanced_batch_async_resolver.py
new file mode 100644
index 00000000000..e56802cf0c6
--- /dev/null
+++ b/examples/event_handler_graphql/src/advanced_batch_async_resolver.py
@@ -0,0 +1,39 @@
+from __future__ import annotations
+
+from typing import Any
+
+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()
+
+# mimic DB data for simplicity
+posts_related = {
+ "1": {"title": "post1"},
+ "2": {"title": "post2"},
+ "3": {"title": "post3"},
+}
+
+
+async def search_batch_posts(posts: list) -> dict[str, Any]:
+ return {post_id: posts_related.get(post_id) for post_id in posts}
+
+
+@app.async_batch_resolver(type_name="Query", field_name="relatedPosts")
+async def related_posts(event: list[AppSyncResolverEvent]) -> list[Any]:
+ # Extract all post_ids in order
+ post_ids: list = [record.source.get("post_id") for record in event]
+
+ # Get unique post_ids while preserving order
+ unique_post_ids = list(dict.fromkeys(post_ids))
+
+ # Fetch posts in a single batch operation
+ fetched_posts = await search_batch_posts(unique_post_ids)
+
+ # Return results in original order
+ return [fetched_posts.get(post_id) for post_id in post_ids]
+
+
+def lambda_handler(event, context: LambdaContext) -> dict:
+ return app.resolve(event, context) # (1)!
diff --git a/examples/event_handler_graphql/src/advanced_batch_query.graphql b/examples/event_handler_graphql/src/advanced_batch_query.graphql
new file mode 100644
index 00000000000..d89358dcde5
--- /dev/null
+++ b/examples/event_handler_graphql/src/advanced_batch_query.graphql
@@ -0,0 +1,12 @@
+query MyQuery {
+ getPost(post_id: "2") {
+ relatedPosts {
+ post_id
+ author
+ relatedPosts {
+ post_id
+ author
+ }
+ }
+ }
+}
diff --git a/examples/event_handler_graphql/src/advanced_batch_resolver.py b/examples/event_handler_graphql/src/advanced_batch_resolver.py
new file mode 100644
index 00000000000..653ce59775e
--- /dev/null
+++ b/examples/event_handler_graphql/src/advanced_batch_resolver.py
@@ -0,0 +1,39 @@
+from __future__ import annotations
+
+from typing import Any
+
+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()
+
+# mimic DB data for simplicity
+posts_related = {
+ "1": {"title": "post1"},
+ "2": {"title": "post2"},
+ "3": {"title": "post3"},
+}
+
+
+def search_batch_posts(posts: list) -> dict[str, Any]:
+ return {post_id: posts_related.get(post_id) for post_id in posts}
+
+
+@app.batch_resolver(type_name="Query", field_name="relatedPosts")
+def related_posts(event: list[AppSyncResolverEvent]) -> list[Any]: # (1)!
+ # Extract all post_ids in order
+ post_ids: list = [record.source.get("post_id") for record in event] # (2)!
+
+ # Get unique post_ids while preserving order
+ unique_post_ids = list(dict.fromkeys(post_ids))
+
+ # Fetch posts in a single batch operation
+ fetched_posts = search_batch_posts(unique_post_ids)
+
+ # Return results in original order
+ return [fetched_posts.get(post_id) for post_id in post_ids]
+
+
+def lambda_handler(event, context: LambdaContext) -> dict:
+ return app.resolve(event, context)
diff --git a/examples/event_handler_graphql/src/advanced_batch_resolver_handling_error.py b/examples/event_handler_graphql/src/advanced_batch_resolver_handling_error.py
new file mode 100644
index 00000000000..a4862c6e55b
--- /dev/null
+++ b/examples/event_handler_graphql/src/advanced_batch_resolver_handling_error.py
@@ -0,0 +1,25 @@
+from typing import Any, Dict
+
+from aws_lambda_powertools import Logger
+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
+
+logger = Logger()
+app = AppSyncResolver()
+
+
+posts_related = {
+ "1": {"title": "post1"},
+ "2": {"title": "post2"},
+ "3": {"title": "post3"},
+}
+
+
+@app.batch_resolver(type_name="Query", field_name="relatedPosts", aggregate=False, raise_on_error=True) # (1)!
+def related_posts(event: AppSyncResolverEvent, post_id: str = "") -> Dict[str, Any]:
+ return posts_related[post_id]
+
+
+def lambda_handler(event, context: LambdaContext) -> dict:
+ return app.resolve(event, context)
diff --git a/examples/event_handler_graphql/src/advanced_batch_resolver_individual.py b/examples/event_handler_graphql/src/advanced_batch_resolver_individual.py
new file mode 100644
index 00000000000..731ec11813f
--- /dev/null
+++ b/examples/event_handler_graphql/src/advanced_batch_resolver_individual.py
@@ -0,0 +1,25 @@
+from typing import Any, Dict
+
+from aws_lambda_powertools import Logger
+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
+
+logger = Logger()
+app = AppSyncResolver()
+
+
+posts_related = {
+ "1": {"title": "post1"},
+ "2": {"title": "post2"},
+ "3": {"title": "post3"},
+}
+
+
+@app.batch_resolver(type_name="Query", field_name="relatedPosts", aggregate=False) # (1)!
+def related_posts(event: AppSyncResolverEvent, post_id: str = "") -> Dict[str, Any]:
+ return posts_related[post_id]
+
+
+def lambda_handler(event, context: LambdaContext) -> dict:
+ return app.resolve(event, context)
diff --git a/examples/event_handler_graphql/src/advanced_batch_resolver_payload.json b/examples/event_handler_graphql/src/advanced_batch_resolver_payload.json
new file mode 100644
index 00000000000..6f4aaa2ff20
--- /dev/null
+++ b/examples/event_handler_graphql/src/advanced_batch_resolver_payload.json
@@ -0,0 +1,59 @@
+[
+ {
+ "arguments":{},
+ "identity":"None",
+ "source":{
+ "post_id":"1",
+ "author":"Author1"
+ },
+ "prev":"None",
+ "info":{
+ "selectionSetList":[
+ "post_id",
+ "author"
+ ],
+ "selectionSetGraphQL":"{\n post_id\n author\n}",
+ "fieldName":"relatedPosts",
+ "parentTypeName":"Post",
+ "variables":{}
+ }
+ },
+ {
+ "arguments":{},
+ "identity":"None",
+ "source":{
+ "post_id":"2",
+ "author":"Author2"
+ },
+ "prev":"None",
+ "info":{
+ "selectionSetList":[
+ "post_id",
+ "author"
+ ],
+ "selectionSetGraphQL":"{\n post_id\n author\n}",
+ "fieldName":"relatedPosts",
+ "parentTypeName":"Post",
+ "variables":{}
+ }
+ },
+ {
+ "arguments":{},
+ "identity":"None",
+ "source":{
+ "post_id":"1",
+ "author":"Author1"
+ },
+ "prev":"None",
+ "info":{
+ "selectionSetList":[
+ "post_id",
+ "author"
+ ],
+ "selectionSetGraphQL":"{\n post_id\n author\n}",
+ "fieldName":"relatedPosts",
+ "parentTypeName":"Post",
+ "variables":{}
+ }
+ }
+]
diff --git a/examples/event_handler_graphql/src/custom_models.py b/examples/event_handler_graphql/src/custom_models.py
index 61e03318d14..84e920af280 100644
--- a/examples/event_handler_graphql/src/custom_models.py
+++ b/examples/event_handler_graphql/src/custom_models.py
@@ -36,7 +36,8 @@ def api_key(self) -> str:
@app.resolver(type_name="Query", field_name="listLocations")
def list_locations(page: int = 0, size: int = 10) -> List[Location]:
# additional properties/methods will now be available under current_event
- logger.debug(f"Request country origin: {app.current_event.country_viewer}") # type: ignore[attr-defined]
+ if app.current_event:
+ logger.debug(f"Request country origin: {app.current_event.country_viewer}") # type: ignore[attr-defined]
return [{"id": scalar_types_utils.make_id(), "name": "Perry, James and Carroll"}]
diff --git a/examples/event_handler_graphql/src/enable_exceptions_batch_resolver.py b/examples/event_handler_graphql/src/enable_exceptions_batch_resolver.py
new file mode 100644
index 00000000000..f77374527ea
--- /dev/null
+++ b/examples/event_handler_graphql/src/enable_exceptions_batch_resolver.py
@@ -0,0 +1,32 @@
+from typing import Dict, 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()
+
+
+posts_related = {
+ "1": {"title": "post1"},
+ "2": {"title": "post2"},
+ "3": {"title": "post3"},
+}
+
+
+class PostRelatedNotFound(Exception):
+ ...
+
+
+@app.batch_resolver(type_name="Query", field_name="relatedPosts", raise_on_error=True) # (1)!
+def related_posts(event: AppSyncResolverEvent, post_id: str) -> Optional[Dict]:
+ post_found = posts_related.get(post_id, None)
+
+ if not post_found:
+ raise PostRelatedNotFound(f"Unable to find a related post with ID {post_id}.")
+
+ return post_found
+
+
+def lambda_handler(event, context: LambdaContext) -> dict:
+ return app.resolve(event, context)
diff --git a/examples/event_handler_graphql/src/enable_exceptions_batch_resolver_payload.json b/examples/event_handler_graphql/src/enable_exceptions_batch_resolver_payload.json
new file mode 100644
index 00000000000..c0de86728ea
--- /dev/null
+++ b/examples/event_handler_graphql/src/enable_exceptions_batch_resolver_payload.json
@@ -0,0 +1,52 @@
+[
+ {
+ "arguments":{
+ "post_id":"12"
+ },
+ "identity":"None",
+ "source":"None",
+ "request":{
+ "headers":{
+ "x-forwarded-for":"18.68.31.36",
+ "sec-ch-ua-mobile":"?0"
+ }
+ },
+ "prev":"None",
+ "info":{
+ "fieldName":"relatedPosts",
+ "selectionSetList":[
+ "relatedPosts"
+ ],
+ "selectionSetGraphQL":"{\n relatedPosts {\n downs\n content\n relatedPosts {\n ups\n downs\n relatedPosts {\n content\n downs\n }\n }\n }\n}",
+ "parentTypeName":"Query",
+ "variables":{
+
+ }
+ }
+ },
+ {
+ "arguments":{
+ "post_id":"1"
+ },
+ "identity":"None",
+ "source":"None",
+ "request":{
+ "headers":{
+ "x-forwarded-for":"18.68.31.36",
+ "sec-ch-ua-mobile":"?0"
+ }
+ },
+ "prev":"None",
+ "info":{
+ "fieldName":"relatedPosts",
+ "selectionSetList":[
+ "relatedPosts"
+ ],
+ "selectionSetGraphQL":"{\n relatedPosts {\n downs\n content\n relatedPosts {\n ups\n downs\n relatedPosts {\n content\n downs\n }\n }\n }\n}",
+ "parentTypeName":"Query",
+ "variables":{
+
+ }
+ }
+ }
+ ]
diff --git a/examples/event_handler_graphql/src/split_operation_append_context_module.py b/examples/event_handler_graphql/src/split_operation_append_context_module.py
index 15ed7af1b9e..c12c22fb626 100644
--- a/examples/event_handler_graphql/src/split_operation_append_context_module.py
+++ b/examples/event_handler_graphql/src/split_operation_append_context_module.py
@@ -1,7 +1,7 @@
from typing import List
from aws_lambda_powertools import Logger, Tracer
-from aws_lambda_powertools.event_handler.appsync import Router
+from aws_lambda_powertools.event_handler.graphql_appsync.router import Router
from aws_lambda_powertools.shared.types import TypedDict
tracer = Tracer()
diff --git a/examples/event_handler_graphql/src/split_operation_module.py b/examples/event_handler_graphql/src/split_operation_module.py
index e4c7f978b73..5d7f9425c97 100644
--- a/examples/event_handler_graphql/src/split_operation_module.py
+++ b/examples/event_handler_graphql/src/split_operation_module.py
@@ -1,7 +1,7 @@
from typing import List
from aws_lambda_powertools import Logger, Tracer
-from aws_lambda_powertools.event_handler.appsync import Router
+from aws_lambda_powertools.event_handler.graphql_appsync.router import Router
from aws_lambda_powertools.shared.types import TypedDict
tracer = Tracer()
diff --git a/examples/homepage/install/sar/cdk_sar.py b/examples/homepage/install/sar/cdk_sar.py
index ff7c8cc40f5..49fb146df6f 100644
--- a/examples/homepage/install/sar/cdk_sar.py
+++ b/examples/homepage/install/sar/cdk_sar.py
@@ -3,7 +3,7 @@
POWERTOOLS_BASE_NAME = "AWSLambdaPowertools"
# Find latest from github.com/aws-powertools/powertools-lambda-python/releases
-POWERTOOLS_VER = "2.37.0"
+POWERTOOLS_VER = "2.39.1"
POWERTOOLS_ARN = "arn:aws:serverlessrepo:eu-west-1:057560766410:applications/aws-lambda-powertools-python-layer"
diff --git a/examples/logger/sam/template.yaml b/examples/logger/sam/template.yaml
index be4a4a5f4aa..7b07e02de59 100644
--- a/examples/logger/sam/template.yaml
+++ b/examples/logger/sam/template.yaml
@@ -14,7 +14,7 @@ Globals:
Layers:
# Find the latest Layer version in the official documentation
# https://docs.powertools.aws.dev/lambda/python/latest/#lambda-layer
- - !Sub arn:aws:lambda:${AWS::Region}:017000801446:layer:AWSLambdaPowertoolsPythonV2:72
+ - !Sub arn:aws:lambda:${AWS::Region}:017000801446:layer:AWSLambdaPowertoolsPythonV2:73
Resources:
LoggerLambdaHandlerExample:
diff --git a/examples/metrics/sam/template.yaml b/examples/metrics/sam/template.yaml
index cf0c7b6f74f..3799e9cd52c 100644
--- a/examples/metrics/sam/template.yaml
+++ b/examples/metrics/sam/template.yaml
@@ -15,7 +15,7 @@ Globals:
Layers:
# Find the latest Layer version in the official documentation
# https://docs.powertools.aws.dev/lambda/python/latest/#lambda-layer
- - !Sub arn:aws:lambda:${AWS::Region}:017000801446:layer:AWSLambdaPowertoolsPythonV2:72
+ - !Sub arn:aws:lambda:${AWS::Region}:017000801446:layer:AWSLambdaPowertoolsPythonV2:73
Resources:
CaptureLambdaHandlerExample:
diff --git a/examples/tracer/sam/template.yaml b/examples/tracer/sam/template.yaml
index d3c0e9dc892..443b610805a 100644
--- a/examples/tracer/sam/template.yaml
+++ b/examples/tracer/sam/template.yaml
@@ -13,7 +13,7 @@ Globals:
Layers:
# Find the latest Layer version in the official documentation
# https://docs.powertools.aws.dev/lambda/python/latest/#lambda-layer
- - !Sub arn:aws:lambda:${AWS::Region}:017000801446:layer:AWSLambdaPowertoolsPythonV2:72
+ - !Sub arn:aws:lambda:${AWS::Region}:017000801446:layer:AWSLambdaPowertoolsPythonV2:73
Resources:
CaptureLambdaHandlerExample:
diff --git a/examples/validation/src/custom_handlers.py b/examples/validation/src/custom_handlers.py
new file mode 100644
index 00000000000..4cbc5d65b93
--- /dev/null
+++ b/examples/validation/src/custom_handlers.py
@@ -0,0 +1,14 @@
+from custom_handlers_schema import CHILD_SCHEMA, PARENT_SCHEMA
+
+from aws_lambda_powertools.utilities.typing import LambdaContext
+from aws_lambda_powertools.utilities.validation import validator
+
+
+# Function to return the child schema
+def get_child_schema(uri: str):
+ return CHILD_SCHEMA
+
+
+@validator(inbound_schema=PARENT_SCHEMA, inbound_handlers={"https": get_child_schema})
+def lambda_handler(event, context: LambdaContext) -> dict:
+ return event
diff --git a/examples/validation/src/custom_handlers_payload.json b/examples/validation/src/custom_handlers_payload.json
new file mode 100644
index 00000000000..09ab994f892
--- /dev/null
+++ b/examples/validation/src/custom_handlers_payload.json
@@ -0,0 +1,6 @@
+{
+ "ParentSchema":
+ {
+ "project": "powertools"
+ }
+}
diff --git a/examples/validation/src/custom_handlers_schema.py b/examples/validation/src/custom_handlers_schema.py
new file mode 100644
index 00000000000..ab911e3d63f
--- /dev/null
+++ b/examples/validation/src/custom_handlers_schema.py
@@ -0,0 +1,22 @@
+PARENT_SCHEMA = {
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "$id": "https://example.com/schemas/parent.json",
+ "type": "object",
+ "properties": {
+ "ParentSchema": {
+ "$ref": "https://SCHEMA",
+ },
+ },
+}
+
+CHILD_SCHEMA = {
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "$id": "https://example.com/schemas/child.json",
+ "type": "object",
+ "properties": {
+ "project": {
+ "type": "string",
+ },
+ },
+ "required": ["project"],
+}
diff --git a/layer/poetry.lock b/layer/poetry.lock
index 1d4a35b8b6c..cedd0656dbb 100644
--- a/layer/poetry.lock
+++ b/layer/poetry.lock
@@ -1,4 +1,4 @@
-# This file is automatically @generated by Poetry 1.7.0 and should not be changed by hand.
+# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand.
[[package]]
name = "attrs"
@@ -411,13 +411,13 @@ files = [
[[package]]
name = "urllib3"
-version = "1.26.18"
+version = "1.26.19"
description = "HTTP library with thread-safe connection pooling, file post, and more."
optional = false
-python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*"
+python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7"
files = [
- {file = "urllib3-1.26.18-py2.py3-none-any.whl", hash = "sha256:34b97092d7e0a3a8cf7cd10e386f401b3737364026c45e622aa02903dffe0f07"},
- {file = "urllib3-1.26.18.tar.gz", hash = "sha256:f8ecc1bba5667413457c529ab955bf8c67b45db799d159066261719e328580a0"},
+ {file = "urllib3-1.26.19-py2.py3-none-any.whl", hash = "sha256:37a0344459b199fce0e80b0d3569837ec6b6937435c5244e7fd73fa6006830f3"},
+ {file = "urllib3-1.26.19.tar.gz", hash = "sha256:3e3d753a8618b86d7de333b4223005f68720bcd6a7d2bcb9fbd2229ec7c1e429"},
]
[package.extras]
@@ -425,23 +425,6 @@ brotli = ["brotli (==1.0.9)", "brotli (>=1.0.9)", "brotlicffi (>=0.8.0)", "brotl
secure = ["certifi", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "ipaddress", "pyOpenSSL (>=0.14)", "urllib3-secure-extra"]
socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"]
-[[package]]
-name = "urllib3"
-version = "2.0.7"
-description = "HTTP library with thread-safe connection pooling, file post, and more."
-optional = false
-python-versions = ">=3.7"
-files = [
- {file = "urllib3-2.0.7-py3-none-any.whl", hash = "sha256:fdb6d215c776278489906c2f8916e6e7d4f5a9b602ccbcfdf7f016fc8da0596e"},
- {file = "urllib3-2.0.7.tar.gz", hash = "sha256:c97dfde1f7bd43a71c8d2a58e369e9b2bf692d1334ea9f9cae55add7d0dd0f84"},
-]
-
-[package.extras]
-brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"]
-secure = ["certifi", "cryptography (>=1.9)", "idna (>=2.0.0)", "pyopenssl (>=17.1.0)", "urllib3-secure-extra"]
-socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"]
-zstd = ["zstandard (>=0.18.0)"]
-
[[package]]
name = "zipp"
version = "3.17.0"
diff --git a/layer/scripts/layer-balancer/go.mod b/layer/scripts/layer-balancer/go.mod
index ee761d06c48..c3470426bc6 100644
--- a/layer/scripts/layer-balancer/go.mod
+++ b/layer/scripts/layer-balancer/go.mod
@@ -3,25 +3,25 @@ module layerbalancer
go 1.18
require (
- github.com/aws/aws-sdk-go-v2 v1.27.2
- github.com/aws/aws-sdk-go-v2/config v1.27.18
- github.com/aws/aws-sdk-go-v2/service/lambda v1.54.6
+ github.com/aws/aws-sdk-go-v2 v1.30.0
+ github.com/aws/aws-sdk-go-v2/config v1.27.22
+ github.com/aws/aws-sdk-go-v2/service/lambda v1.56.0
golang.org/x/exp v0.0.0-20230321023759-10a507213a29
golang.org/x/sync v0.7.0
)
require (
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.2 // indirect
- github.com/aws/aws-sdk-go-v2/credentials v1.17.18 // indirect
- github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.5 // indirect
- github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.9 // indirect
- github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.9 // indirect
+ github.com/aws/aws-sdk-go-v2/credentials v1.17.22 // indirect
+ github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.8 // indirect
+ github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.12 // indirect
+ github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.12 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.2 // indirect
- github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.11 // indirect
- github.com/aws/aws-sdk-go-v2/service/sso v1.20.11 // indirect
- github.com/aws/aws-sdk-go-v2/service/ssooidc v1.24.5 // indirect
- github.com/aws/aws-sdk-go-v2/service/sts v1.28.12 // indirect
+ github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.14 // indirect
+ github.com/aws/aws-sdk-go-v2/service/sso v1.22.0 // indirect
+ github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.0 // indirect
+ github.com/aws/aws-sdk-go-v2/service/sts v1.30.0 // indirect
github.com/aws/smithy-go v1.20.2 // indirect
github.com/jmespath/go-jmespath v0.4.0 // indirect
)
diff --git a/layer/scripts/layer-balancer/go.sum b/layer/scripts/layer-balancer/go.sum
index 8b0785301d6..064a942424d 100644
--- a/layer/scripts/layer-balancer/go.sum
+++ b/layer/scripts/layer-balancer/go.sum
@@ -1,31 +1,31 @@
-github.com/aws/aws-sdk-go-v2 v1.27.2 h1:pLsTXqX93rimAOZG2FIYraDQstZaaGVVN4tNw65v0h8=
-github.com/aws/aws-sdk-go-v2 v1.27.2/go.mod h1:ffIFB97e2yNsv4aTSGkqtHnppsIJzw7G7BReUZ3jCXM=
+github.com/aws/aws-sdk-go-v2 v1.30.0 h1:6qAwtzlfcTtcL8NHtbDQAqgM5s6NDipQTkPxyH/6kAA=
+github.com/aws/aws-sdk-go-v2 v1.30.0/go.mod h1:ffIFB97e2yNsv4aTSGkqtHnppsIJzw7G7BReUZ3jCXM=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.2 h1:x6xsQXGSmW6frevwDA+vi/wqhp1ct18mVXYN08/93to=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.2/go.mod h1:lPprDr1e6cJdyYeGXnRaJoP4Md+cDBvi2eOj00BlGmg=
-github.com/aws/aws-sdk-go-v2/config v1.27.18 h1:wFvAnwOKKe7QAyIxziwSKjmer9JBMH1vzIL6W+fYuKk=
-github.com/aws/aws-sdk-go-v2/config v1.27.18/go.mod h1:0xz6cgdX55+kmppvPm2IaKzIXOheGJhAufacPJaXZ7c=
-github.com/aws/aws-sdk-go-v2/credentials v1.17.18 h1:D/ALDWqK4JdY3OFgA2thcPO1c9aYTT5STS/CvnkqY1c=
-github.com/aws/aws-sdk-go-v2/credentials v1.17.18/go.mod h1:JuitCWq+F5QGUrmMPsk945rop6bB57jdscu+Glozdnc=
-github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.5 h1:dDgptDO9dxeFkXy+tEgVkzSClHZje/6JkPW5aZyEvrQ=
-github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.5/go.mod h1:gjvE2KBUgUQhcv89jqxrIxH9GaKs1JbZzWejj/DaHGA=
-github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.9 h1:cy8ahBJuhtM8GTTSyOkfy6WVPV1IE+SS5/wfXUYuulw=
-github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.9/go.mod h1:CZBXGLaJnEZI6EVNcPd7a6B5IC5cA/GkRWtu9fp3S6Y=
-github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.9 h1:A4SYk07ef04+vxZToz9LWvAXl9LW0NClpPpMsi31cz0=
-github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.9/go.mod h1:5jJcHuwDagxN+ErjQ3PU3ocf6Ylc/p9x+BLO/+X4iXw=
+github.com/aws/aws-sdk-go-v2/config v1.27.22 h1:TRkQVtpDINt+Na/ToU7iptyW6U0awAwJ24q4XN+59k8=
+github.com/aws/aws-sdk-go-v2/config v1.27.22/go.mod h1:EYY3mVgFRUWkh6QNKH64MdyKs1YSUgatc0Zp3MDxi7c=
+github.com/aws/aws-sdk-go-v2/credentials v1.17.22 h1:wu9kXQbbt64ul09v3ye4HYleAr4WiGV/uv69EXKDEr0=
+github.com/aws/aws-sdk-go-v2/credentials v1.17.22/go.mod h1:pcvMtPcxJn3r2k6mZD9I0EcumLqPLA7V/0iCgOIlY+o=
+github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.8 h1:FR+oWPFb/8qMVYMWN98bUZAGqPvLHiyqg1wqQGfUAXY=
+github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.8/go.mod h1:EgSKcHiuuakEIxJcKGzVNWh5srVAQ3jKaSrBGRYvM48=
+github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.12 h1:SJ04WXGTwnHlWIODtC5kJzKbeuHt+OUNOgKg7nfnUGw=
+github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.12/go.mod h1:FkpvXhA92gb3GE9LD6Og0pHHycTxW7xGpnEh5E7Opwo=
+github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.12 h1:hb5KgeYfObi5MHkSSZMEudnIvX30iB+E21evI4r6BnQ=
+github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.12/go.mod h1:CroKe/eWJdyfy9Vx4rljP5wTUjNJfb+fPz1uMYUhEGM=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0 h1:hT8rVHwugYE2lEfdFE0QWVo81lF7jMrYJVDWI+f+VxU=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0/go.mod h1:8tu/lYfQfFe6IGnaOdrpVgEL2IrrDOf6/m9RQum4NkY=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.2 h1:Ji0DY1xUsUr3I8cHps0G+XM3WWU16lP6yG8qu1GAZAs=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.2/go.mod h1:5CsjAbs3NlGQyZNFACh+zztPDI7fU6eW9QsxjfnuBKg=
-github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.11 h1:o4T+fKxA3gTMcluBNZZXE9DNaMkJuUL1O3mffCUjoJo=
-github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.11/go.mod h1:84oZdJ+VjuJKs9v1UTC9NaodRZRseOXCTgku+vQJWR8=
-github.com/aws/aws-sdk-go-v2/service/lambda v1.54.6 h1:UMu5aeSubjM9geSuPCGOgBAZa0JvsXxJBFXmKgUuisM=
-github.com/aws/aws-sdk-go-v2/service/lambda v1.54.6/go.mod h1:fWbFM4/v+IgUW+p4TooAXuhmiQyC5qxMV5gUqxDII2g=
-github.com/aws/aws-sdk-go-v2/service/sso v1.20.11 h1:gEYM2GSpr4YNWc6hCd5nod4+d4kd9vWIAWrmGuLdlMw=
-github.com/aws/aws-sdk-go-v2/service/sso v1.20.11/go.mod h1:gVvwPdPNYehHSP9Rs7q27U1EU+3Or2ZpXvzAYJNh63w=
-github.com/aws/aws-sdk-go-v2/service/ssooidc v1.24.5 h1:iXjh3uaH3vsVcnyZX7MqCoCfcyxIrVE9iOQruRaWPrQ=
-github.com/aws/aws-sdk-go-v2/service/ssooidc v1.24.5/go.mod h1:5ZXesEuy/QcO0WUnt+4sDkxhdXRHTu2yG0uCSH8B6os=
-github.com/aws/aws-sdk-go-v2/service/sts v1.28.12 h1:M/1u4HBpwLuMtjlxuI2y6HoVLzF5e2mfxHCg7ZVMYmk=
-github.com/aws/aws-sdk-go-v2/service/sts v1.28.12/go.mod h1:kcfd+eTdEi/40FIbLq4Hif3XMXnl5b/+t/KTfLt9xIk=
+github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.14 h1:zSDPny/pVnkqABXYRicYuPf9z2bTqfH13HT3v6UheIk=
+github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.14/go.mod h1:3TTcI5JSzda1nw/pkVC9dhgLre0SNBFj2lYS4GctXKI=
+github.com/aws/aws-sdk-go-v2/service/lambda v1.56.0 h1:TE7/Fs7TJx0lw3KkAsPzwNphPClaFoLZLWybET9AAw8=
+github.com/aws/aws-sdk-go-v2/service/lambda v1.56.0/go.mod h1:5drdANY67aOvUNJLjBEg2HXeCXkk0MDurqsJs73TXVQ=
+github.com/aws/aws-sdk-go-v2/service/sso v1.22.0 h1:lPIAPCRoJkmotLTU/9B6icUFlYDpEuWjKeL79XROv1M=
+github.com/aws/aws-sdk-go-v2/service/sso v1.22.0/go.mod h1:lcQG/MmxydijbeTOp04hIuJwXGWPZGI3bwdFDGRTv14=
+github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.0 h1:/4r71ghx+hX9spr884cqXHPEmPzqH/J3K7fkE1yfcmw=
+github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.0/go.mod h1:z0P8K+cBIsFXUr5rzo/psUeJ20XjPN0+Nn8067Nd+E4=
+github.com/aws/aws-sdk-go-v2/service/sts v1.30.0 h1:9ja34PaKybhCJjVKvxtDsUjbATUJGN+eF6QnO58u5cI=
+github.com/aws/aws-sdk-go-v2/service/sts v1.30.0/go.mod h1:N2mQiucsO0VwK9CYuS4/c2n6Smeh1v47Rz3dWCPFLdE=
github.com/aws/smithy-go v1.20.2 h1:tbp628ireGtzcHDDmLT/6ADHidqnwgF57XOXZe6tp4Q=
github.com/aws/smithy-go v1.20.2/go.mod h1:krry+ya/rV9RDcV/Q16kpu6ypI4K2czasz0NC3qS14E=
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
diff --git a/mkdocs.yml b/mkdocs.yml
index efd18efd5cc..d2bab86cd22 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -116,7 +116,7 @@ markdown_extensions:
- meta
- toc:
permalink: true
- toc_depth: 4
+ toc_depth: 5
- attr_list
- md_in_html
- pymdownx.emoji:
diff --git a/noxfile.py b/noxfile.py
new file mode 100644
index 00000000000..68882470de5
--- /dev/null
+++ b/noxfile.py
@@ -0,0 +1,201 @@
+# Run nox tests
+#
+# usage:
+# poetry run nox --error-on-external-run --reuse-venv=yes --non-interactive
+#
+# If you want to target a specific Python version, add -p parameter
+
+from typing import List, Optional
+
+import nox
+
+PREFIX_TESTS_FUNCTIONAL = "tests/functional"
+PREFIX_TESTS_UNIT = "tests/unit"
+
+
+def build_and_run_test(session: nox.Session, folders: List, extras: Optional[str] = "") -> None:
+ """
+ This function is responsible for setting up the testing environment and running the test suite for specific feature.
+
+ The function performs the following tasks:
+ 1. Installs the required dependencies for executing any test
+ 2. If the `extras` parameter is provided, the function installs the additional dependencies
+ 3. the function runs the pytest command with the specified folders as arguments, executing the test suite.
+
+ Parameters
+ ----------
+ session: nox.Session
+ The current Nox session object, which is used to manage the virtual environment and execute commands.
+ folders: List
+ A list of folder paths that contain the test files to be executed.
+ extras: Optional[str]
+ A string representing additional dependencies that should be installed for the test environment.
+ If not provided, the function will install the project with basic dependencies
+ """
+
+ # Required install to execute any test
+ session.install("poetry", "pytest", "pytest-mock", "pytest_socket")
+
+ # Powertools project folder is in the root
+ if extras:
+ session.install(f"./[{extras}]")
+ else:
+ session.install("./")
+
+ # Execute test in specific folders
+ session.run("pytest", *folders)
+
+
+@nox.session()
+def test_with_only_required_packages(session: nox.Session):
+ """Tests that only depends for required libraries"""
+ # Logger
+ # Metrics - Amazon CloudWatch EMF
+ # Metrics - Base provider
+ # Middleware factory without tracer
+ # Typing
+ # Data Class - without codepipeline dataclass
+ # Event Handler without OpenAPI
+ # Batch processor - without pydantic integration
+ build_and_run_test(
+ session,
+ folders=[
+ f"{PREFIX_TESTS_FUNCTIONAL}/logger/required_dependencies/",
+ f"{PREFIX_TESTS_FUNCTIONAL}/metrics/required_dependencies/",
+ f"{PREFIX_TESTS_FUNCTIONAL}/middleware_factory/required_dependencies/",
+ f"{PREFIX_TESTS_FUNCTIONAL}/typing/required_dependencies/",
+ f"{PREFIX_TESTS_UNIT}/data_classes/required_dependencies/",
+ f"{PREFIX_TESTS_FUNCTIONAL}/event_handler/required_dependencies/",
+ f"{PREFIX_TESTS_FUNCTIONAL}/batch/required_dependencies/",
+ ],
+ )
+
+
+@nox.session()
+def test_with_datadog_as_required_package(session: nox.Session):
+ """Tests that depends on Datadog library"""
+ # Metrics - Datadog
+ build_and_run_test(
+ session,
+ folders=[
+ f"{PREFIX_TESTS_FUNCTIONAL}/metrics/datadog/",
+ ],
+ extras="datadog",
+ )
+
+
+@nox.session()
+def test_with_xray_sdk_as_required_package(session: nox.Session):
+ """Tests that depends on AWS XRAY SDK library"""
+ # Tracer
+ # Middleware factory with tracer
+ build_and_run_test(
+ session,
+ folders=[
+ f"{PREFIX_TESTS_FUNCTIONAL}/tracer/_aws_xray_sdk/",
+ f"{PREFIX_TESTS_FUNCTIONAL}/middleware_factory/_aws_xray_sdk/",
+ ],
+ extras="tracer",
+ )
+
+
+@nox.session()
+def test_with_boto3_sdk_as_required_package(session: nox.Session):
+ """Tests that depends on boto3/botocore library"""
+ # Parameters
+ # Feature Flags
+ # Data Class - only codepipeline dataclass
+ # Streaming
+ # Idempotency - DynamoDB persistent layer
+ build_and_run_test(
+ session,
+ folders=[
+ f"{PREFIX_TESTS_FUNCTIONAL}/parameters/_boto3/",
+ f"{PREFIX_TESTS_FUNCTIONAL}/feature_flags/_boto3/",
+ f"{PREFIX_TESTS_UNIT}/data_classes/_boto3/",
+ f"{PREFIX_TESTS_FUNCTIONAL}/streaming/_boto3/",
+ f"{PREFIX_TESTS_FUNCTIONAL}/idempotency/_boto3/",
+ ],
+ extras="aws-sdk",
+ )
+
+
+@nox.session()
+def test_with_fastjsonschema_as_required_package(session: nox.Session):
+ """Tests that depends on fastjsonschema library"""
+ # Validation
+ build_and_run_test(
+ session,
+ folders=[
+ f"{PREFIX_TESTS_FUNCTIONAL}/validator/_fastjsonschema/",
+ ],
+ extras="validation",
+ )
+
+
+@nox.session()
+def test_with_aws_encryption_sdk_as_required_package(session: nox.Session):
+ """Tests that depends on aws_encryption_sdk library"""
+ # Data Masking
+ build_and_run_test(
+ session,
+ folders=[
+ f"{PREFIX_TESTS_FUNCTIONAL}/data_masking/_aws_encryption_sdk/",
+ f"{PREFIX_TESTS_UNIT}/data_masking/_aws_encryption_sdk/",
+ ],
+ extras="datamasking",
+ )
+
+
+@nox.session()
+@nox.parametrize("pydantic", ["1.10", "2.0"])
+def test_with_pydantic_required_package(session: nox.Session, pydantic: str):
+ """Tests that only depends for Pydantic library v1 and v2"""
+ # Event Handler OpenAPI
+ # Parser
+ # Batch Processor with pydantic integration
+
+ session.install(f"pydantic>={pydantic}")
+
+ build_and_run_test(
+ session,
+ folders=[
+ f"{PREFIX_TESTS_FUNCTIONAL}/event_handler/_pydantic/",
+ f"{PREFIX_TESTS_FUNCTIONAL}/batch/_pydantic/",
+ f"{PREFIX_TESTS_UNIT}/parser/_pydantic/",
+ ],
+ )
+
+
+@nox.session()
+@nox.parametrize("pydantic", ["1.10", "2.0"])
+def test_with_boto3_and_pydantic_required_package(session: nox.Session, pydantic: str):
+ """Tests that only depends for Boto3 + Pydantic library v1 and v2"""
+ # Idempotency with custom serializer
+
+ session.install(f"pydantic>={pydantic}")
+
+ build_and_run_test(
+ session,
+ folders=[
+ f"{PREFIX_TESTS_FUNCTIONAL}/idempotency/_pydantic/",
+ ],
+ extras="aws-sdk",
+ )
+
+
+@nox.session()
+def test_with_redis_and_boto3_sdk_as_required_package(session: nox.Session):
+ """Tests that depends on Redis library"""
+ # Idempotency - Redis backend
+
+ # Our Redis tests requires multiprocess library to simulate Race Condition
+ session.run("pip", "install", "multiprocess")
+
+ build_and_run_test(
+ session,
+ folders=[
+ f"{PREFIX_TESTS_FUNCTIONAL}/idempotency/_redis/",
+ ],
+ extras="redis,aws-sdk",
+ )
diff --git a/package-lock.json b/package-lock.json
index 71fec4ea8eb..0a1a80da573 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -15,9 +15,9 @@
}
},
"node_modules/aws-cdk": {
- "version": "2.145.0",
- "resolved": "https://registry.npmjs.org/aws-cdk/-/aws-cdk-2.145.0.tgz",
- "integrity": "sha512-Jdw7nbrXiihYM/jReXK0/i8a+W/o+fLcn1f8Yzvns1jP58KBGQygqyiv5Dm+uqzS3D8/ZZnfPu3ph6aOVLPNSA==",
+ "version": "2.147.1",
+ "resolved": "https://registry.npmjs.org/aws-cdk/-/aws-cdk-2.147.1.tgz",
+ "integrity": "sha512-HKlm9j62P/JkYtdTuv0RfZrZyzxnTJwumkaHa//FAAFkZCI+gIwobaUJzlLQh+cK+0hDG98n9Yyw6dOIYgnTLQ==",
"dev": true,
"bin": {
"cdk": "bin/cdk"
diff --git a/poetry.lock b/poetry.lock
index ced0e9ee99e..ad7a251add1 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -1,4 +1,4 @@
-# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand.
+# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand.
[[package]]
name = "anyio"
@@ -22,6 +22,20 @@ doc = ["Sphinx (>=7)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphin
test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.17)"]
trio = ["trio (>=0.23)"]
+[[package]]
+name = "argcomplete"
+version = "3.4.0"
+description = "Bash tab completion for argparse"
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "argcomplete-3.4.0-py3-none-any.whl", hash = "sha256:69a79e083a716173e5532e0fa3bef45f793f4e61096cf52b5a42c0211c8b8aa5"},
+ {file = "argcomplete-3.4.0.tar.gz", hash = "sha256:c2abcdfe1be8ace47ba777d4fce319eb13bf8ad9dace8d085dcad6eded88057f"},
+]
+
+[package.extras]
+test = ["coverage", "mypy", "pexpect", "ruff", "wheel"]
+
[[package]]
name = "async-timeout"
version = "4.0.3"
@@ -156,33 +170,51 @@ jsii = ">=1.92.0,<2.0.0"
publication = ">=0.0.3"
typeguard = ">=2.13.3,<2.14.0"
+[[package]]
+name = "aws-cdk-aws-appsync-alpha"
+version = "2.59.0a0"
+description = "The CDK Construct Library for AWS::AppSync"
+optional = false
+python-versions = "~=3.7"
+files = [
+ {file = "aws-cdk.aws-appsync-alpha-2.59.0a0.tar.gz", hash = "sha256:f5c7773b70b759efd576561dc3d71af5762a6f7cbc9ee9eef5e538c7ab3dccc7"},
+ {file = "aws_cdk.aws_appsync_alpha-2.59.0a0-py3-none-any.whl", hash = "sha256:ecc235f1f70d404c8d03cf250be0227becd14c468f8c43b6d9df334a1d60c8e2"},
+]
+
+[package.dependencies]
+aws-cdk-lib = ">=2.59.0,<3.0.0"
+constructs = ">=10.0.0,<11.0.0"
+jsii = ">=1.72.0,<2.0.0"
+publication = ">=0.0.3"
+typeguard = ">=2.13.3,<2.14.0"
+
[[package]]
name = "aws-cdk-aws-lambda-python-alpha"
-version = "2.145.0a0"
+version = "2.147.1a0"
description = "The CDK Construct Library for AWS Lambda in Python"
optional = false
python-versions = "~=3.8"
files = [
- {file = "aws-cdk.aws-lambda-python-alpha-2.145.0a0.tar.gz", hash = "sha256:03e5f1474b784ce44a5a558de1f653ac1b13d3c717bd92a9b30e5929c61c4da8"},
- {file = "aws_cdk.aws_lambda_python_alpha-2.145.0a0-py3-none-any.whl", hash = "sha256:62fcec993df90ee56c84f823bf96bc2845f7b507758b4d7a64348a14d8511f7a"},
+ {file = "aws-cdk.aws-lambda-python-alpha-2.147.1a0.tar.gz", hash = "sha256:30773f2865ba58396090b6209e906d1c508bf297b99a316f234227143b1ef6f7"},
+ {file = "aws_cdk.aws_lambda_python_alpha-2.147.1a0-py3-none-any.whl", hash = "sha256:b7e47e9d45be643d2bf08f2a675a0a18f311e430343d3155b020068e0917409e"},
]
[package.dependencies]
-aws-cdk-lib = ">=2.145.0,<3.0.0"
+aws-cdk-lib = ">=2.147.1,<3.0.0"
constructs = ">=10.0.0,<11.0.0"
-jsii = ">=1.98.0,<2.0.0"
+jsii = ">=1.99.0,<2.0.0"
publication = ">=0.0.3"
typeguard = ">=2.13.3,<2.14.0"
[[package]]
name = "aws-cdk-lib"
-version = "2.145.0"
+version = "2.147.1"
description = "Version 2 of the AWS Cloud Development Kit library"
optional = false
python-versions = "~=3.8"
files = [
- {file = "aws-cdk-lib-2.145.0.tar.gz", hash = "sha256:be9dc62747e6a2fe6b91f77675fbf8f8daed2c3e10174b860a831128045437dd"},
- {file = "aws_cdk_lib-2.145.0-py3-none-any.whl", hash = "sha256:92261768346ec02d958978124e474bb41674ee3630580fdbf034a593a1c5bbc7"},
+ {file = "aws-cdk-lib-2.147.1.tar.gz", hash = "sha256:2c931059eeb731843861daff54f6d3551c56d6c938f3149b1171133201148341"},
+ {file = "aws_cdk_lib-2.147.1-py3-none-any.whl", hash = "sha256:64c763b4d9aeb5528b5778afcde5e9af6126952ca06f0f4adf66568b037d86fc"},
]
[package.dependencies]
@@ -190,7 +222,7 @@ files = [
"aws-cdk.asset-kubectl-v20" = ">=2.1.2,<3.0.0"
"aws-cdk.asset-node-proxy-agent-v6" = ">=2.0.3,<3.0.0"
constructs = ">=10.0.0,<11.0.0"
-jsii = ">=1.98.0,<2.0.0"
+jsii = ">=1.99.0,<2.0.0"
publication = ">=0.0.3"
typeguard = ">=2.13.3,<2.14.0"
@@ -279,13 +311,13 @@ dev = ["freezegun (>=1.0,<2.0)", "pytest (>=6.0)", "pytest-cov"]
[[package]]
name = "bandit"
-version = "1.7.8"
+version = "1.7.9"
description = "Security oriented static analyser for python code."
optional = false
python-versions = ">=3.8"
files = [
- {file = "bandit-1.7.8-py3-none-any.whl", hash = "sha256:509f7af645bc0cd8fd4587abc1a038fc795636671ee8204d502b933aee44f381"},
- {file = "bandit-1.7.8.tar.gz", hash = "sha256:36de50f720856ab24a24dbaa5fee2c66050ed97c1477e0a1159deab1775eab6b"},
+ {file = "bandit-1.7.9-py3-none-any.whl", hash = "sha256:52077cb339000f337fb25f7e045995c4ad01511e716e5daac37014b9752de8ec"},
+ {file = "bandit-1.7.9.tar.gz", hash = "sha256:7c395a436743018f7be0a4cbb0a4ea9b902b6d87264ddecf8cfdc73b4f78ff61"},
]
[package.dependencies]
@@ -349,17 +381,17 @@ uvloop = ["uvloop (>=0.15.2)"]
[[package]]
name = "boto3"
-version = "1.34.114"
+version = "1.34.134"
description = "The AWS SDK for Python"
optional = false
python-versions = ">=3.8"
files = [
- {file = "boto3-1.34.114-py3-none-any.whl", hash = "sha256:4460958d2b0c53bd2195b23ed5d45db2350e514486fe8caeb38b285b30742280"},
- {file = "boto3-1.34.114.tar.gz", hash = "sha256:eeb11bca9b19d12baf93436fb8a16b8b824f1f7e8b9bcc722607e862c46b1b08"},
+ {file = "boto3-1.34.134-py3-none-any.whl", hash = "sha256:342782c02ff077aae118c9c61179eed95c585831fba666baacc5588ff04aa6e1"},
+ {file = "boto3-1.34.134.tar.gz", hash = "sha256:f6d6e5b0c9ab022a75373fa16c01f0cd54bc1bb64ef3b6ac64ac7cedd56cbe9c"},
]
[package.dependencies]
-botocore = ">=1.34.114,<1.35.0"
+botocore = ">=1.34.134,<1.35.0"
jmespath = ">=0.7.1,<2.0.0"
s3transfer = ">=0.10.0,<0.11.0"
@@ -368,13 +400,13 @@ crt = ["botocore[crt] (>=1.21.0,<2.0a0)"]
[[package]]
name = "botocore"
-version = "1.34.114"
+version = "1.34.134"
description = "Low-level, data-driven core of boto 3."
optional = false
python-versions = ">=3.8"
files = [
- {file = "botocore-1.34.114-py3-none-any.whl", hash = "sha256:606d1e55984d45e41a812badee292755f4db0233eed9cca63ea3bb8f5755507f"},
- {file = "botocore-1.34.114.tar.gz", hash = "sha256:5705f74fda009656a218ffaf4afd81228359160f2ab806ab8222d07e9da3a73b"},
+ {file = "botocore-1.34.134-py3-none-any.whl", hash = "sha256:45219e00639755f92569b29f8f279d5dde721494791412c1f7026a3779e8d9f4"},
+ {file = "botocore-1.34.134.tar.gz", hash = "sha256:e29c299599426ed16dd2d4c1e20eef784f96b15e1850ebbc59a3250959285b95"},
]
[package.dependencies]
@@ -386,7 +418,7 @@ urllib3 = [
]
[package.extras]
-crt = ["awscrt (==0.20.9)"]
+crt = ["awscrt (==0.20.11)"]
[[package]]
name = "bytecode"
@@ -429,50 +461,50 @@ ujson = ["ujson (>=5.7.0)"]
[[package]]
name = "cdk-nag"
-version = "2.28.140"
+version = "2.28.150"
description = "Check CDK v2 applications for best practices using a combination on available rule packs."
optional = false
python-versions = "~=3.8"
files = [
- {file = "cdk-nag-2.28.140.tar.gz", hash = "sha256:80262ab5f0d2a0de4cebcccdf655458dd36c93493ba39b475623b8decd0f29f6"},
- {file = "cdk_nag-2.28.140-py3-none-any.whl", hash = "sha256:6da6ce1f01b5fd645815510bbebbcde9e4028f36bbb820fb05a96f5b3093ddcd"},
+ {file = "cdk-nag-2.28.150.tar.gz", hash = "sha256:177be19d7f481c7020d32583fe0082d3def9678f4df26951b23912292f02f0b4"},
+ {file = "cdk_nag-2.28.150-py3-none-any.whl", hash = "sha256:0d77e9ce7986eca3be42e3b546c4b9c7786eb1ae8132e4d4adabc9b3295da9f7"},
]
[package.dependencies]
aws-cdk-lib = ">=2.116.0,<3.0.0"
constructs = ">=10.0.5,<11.0.0"
-jsii = ">=1.99.0,<2.0.0"
+jsii = ">=1.101.0,<2.0.0"
publication = ">=0.0.3"
typeguard = ">=2.13.3,<2.14.0"
[[package]]
name = "cdklabs-generative-ai-cdk-constructs"
-version = "0.1.179"
+version = "0.1.198"
description = "AWS Generative AI CDK Constructs is a library for well-architected generative AI patterns."
optional = false
python-versions = "~=3.8"
files = [
- {file = "cdklabs.generative-ai-cdk-constructs-0.1.179.tar.gz", hash = "sha256:60066b67fbee2ed22ccaef041d8b5179c550f0900854c62c46bce9f07e85153e"},
- {file = "cdklabs.generative_ai_cdk_constructs-0.1.179-py3-none-any.whl", hash = "sha256:e6601cbb754ad5e375429f9abc14db45196c85b10ccc2bde836ebd4285200a73"},
+ {file = "cdklabs.generative-ai-cdk-constructs-0.1.198.tar.gz", hash = "sha256:1f6ae4e910369158590fe47ae087f2b03eacfbe55ba9212156214621bf45d166"},
+ {file = "cdklabs.generative_ai_cdk_constructs-0.1.198-py3-none-any.whl", hash = "sha256:39c9af08cfc9cf9d05dbcea335fdb762ff738d56202f77c81e25d2c1a113ef46"},
]
[package.dependencies]
aws-cdk-lib = ">=2.143.0,<3.0.0"
-cdk-nag = ">=2.28.139,<3.0.0"
+cdk-nag = ">=2.28.145,<3.0.0"
constructs = ">=10.3.0,<11.0.0"
-jsii = ">=1.99.0,<2.0.0"
+jsii = ">=1.100.0,<2.0.0"
publication = ">=0.0.3"
typeguard = ">=2.13.3,<2.14.0"
[[package]]
name = "certifi"
-version = "2024.2.2"
+version = "2024.6.2"
description = "Python package for providing Mozilla's CA Bundle."
optional = false
python-versions = ">=3.6"
files = [
- {file = "certifi-2024.2.2-py3-none-any.whl", hash = "sha256:dc383c07b76109f368f6106eee2b593b04a011ea4d55f652c6ca24a754d1cdd1"},
- {file = "certifi-2024.2.2.tar.gz", hash = "sha256:0569859f95fc761b18b45ef421b1290a0f65f147e92a1e5eb3e635f9a5e4e66f"},
+ {file = "certifi-2024.6.2-py3-none-any.whl", hash = "sha256:ddc6c8ce995e6987e7faf5e3f1b02b302836a0e5d98ece18392cb1a36c72ad56"},
+ {file = "certifi-2024.6.2.tar.gz", hash = "sha256:3cd43f1c6fa7dedc5899d69d3ad0398fd018ad1a17fba83ddaf78aa46c747516"},
]
[[package]]
@@ -541,26 +573,28 @@ pycparser = "*"
[[package]]
name = "cfn-lint"
-version = "0.87.6"
+version = "1.3.7"
description = "Checks CloudFormation templates for practices and behaviour that could potentially be improved"
optional = false
-python-versions = "!=4.0,<=4.0,>=3.8"
+python-versions = ">=3.8"
files = [
- {file = "cfn_lint-0.87.6-py3-none-any.whl", hash = "sha256:ee889cdc0f76a419efd1c20105ae8675b8af1de8e689928d59ffec33a141c1a6"},
- {file = "cfn_lint-0.87.6.tar.gz", hash = "sha256:aba311b0dae2be77422e8152974da2047f048d24a287b16d9c78b234d0714592"},
+ {file = "cfn_lint-1.3.7-py3-none-any.whl", hash = "sha256:5a75b952eebbe2feee2ceec77ba74b0a3b9b846115597bd25aa87b63feab0c72"},
+ {file = "cfn_lint-1.3.7.tar.gz", hash = "sha256:be7229290945f91302cef12e76ceb8120939d3f738a9f29b6b7d5a9d90c4f1b8"},
]
[package.dependencies]
aws-sam-translator = ">=1.89.0"
-jschema-to-python = ">=1.2.3,<1.3.0"
jsonpatch = "*"
-jsonschema = ">=3.0,<5"
-junit-xml = ">=1.9,<2.0"
networkx = ">=2.4,<4"
pyyaml = ">5.4"
-regex = ">=2021.7.1"
-sarif-om = ">=1.0.4,<1.1.0"
+regex = "*"
sympy = ">=1.0.0"
+typing-extensions = "*"
+
+[package.extras]
+graph = ["pydot"]
+junit = ["junit-xml (>=1.9,<2.0)"]
+sarif = ["jschema-to-python (>=1.2.3,<1.3.0)", "sarif-om (>=1.0.4,<1.1.0)"]
[[package]]
name = "charset-normalizer"
@@ -697,6 +731,23 @@ files = [
{file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
]
+[[package]]
+name = "colorlog"
+version = "6.8.2"
+description = "Add colours to the output of Python's logging module."
+optional = false
+python-versions = ">=3.6"
+files = [
+ {file = "colorlog-6.8.2-py3-none-any.whl", hash = "sha256:4dcbb62368e2800cb3c5abd348da7e53f6c362dda502ec27c560b2e58a66bd33"},
+ {file = "colorlog-6.8.2.tar.gz", hash = "sha256:3e3e079a41feb5a1b64f978b5ea4f46040a94f11f0e8bbb8261e3dbbeca64d44"},
+]
+
+[package.dependencies]
+colorama = {version = "*", markers = "sys_platform == \"win32\""}
+
+[package.extras]
+development = ["black", "flake8", "mypy", "pytest", "types-colorama"]
+
[[package]]
name = "constructs"
version = "10.3.0"
@@ -715,63 +766,63 @@ typeguard = ">=2.13.3,<2.14.0"
[[package]]
name = "coverage"
-version = "7.5.3"
+version = "7.5.4"
description = "Code coverage measurement for Python"
optional = false
python-versions = ">=3.8"
files = [
- {file = "coverage-7.5.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a6519d917abb15e12380406d721e37613e2a67d166f9fb7e5a8ce0375744cd45"},
- {file = "coverage-7.5.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:aea7da970f1feccf48be7335f8b2ca64baf9b589d79e05b9397a06696ce1a1ec"},
- {file = "coverage-7.5.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:923b7b1c717bd0f0f92d862d1ff51d9b2b55dbbd133e05680204465f454bb286"},
- {file = "coverage-7.5.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:62bda40da1e68898186f274f832ef3e759ce929da9a9fd9fcf265956de269dbc"},
- {file = "coverage-7.5.3-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d8b7339180d00de83e930358223c617cc343dd08e1aa5ec7b06c3a121aec4e1d"},
- {file = "coverage-7.5.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:25a5caf742c6195e08002d3b6c2dd6947e50efc5fc2c2205f61ecb47592d2d83"},
- {file = "coverage-7.5.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:05ac5f60faa0c704c0f7e6a5cbfd6f02101ed05e0aee4d2822637a9e672c998d"},
- {file = "coverage-7.5.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:239a4e75e09c2b12ea478d28815acf83334d32e722e7433471fbf641c606344c"},
- {file = "coverage-7.5.3-cp310-cp310-win32.whl", hash = "sha256:a5812840d1d00eafae6585aba38021f90a705a25b8216ec7f66aebe5b619fb84"},
- {file = "coverage-7.5.3-cp310-cp310-win_amd64.whl", hash = "sha256:33ca90a0eb29225f195e30684ba4a6db05dbef03c2ccd50b9077714c48153cac"},
- {file = "coverage-7.5.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f81bc26d609bf0fbc622c7122ba6307993c83c795d2d6f6f6fd8c000a770d974"},
- {file = "coverage-7.5.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7cec2af81f9e7569280822be68bd57e51b86d42e59ea30d10ebdbb22d2cb7232"},
- {file = "coverage-7.5.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:55f689f846661e3f26efa535071775d0483388a1ccfab899df72924805e9e7cd"},
- {file = "coverage-7.5.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:50084d3516aa263791198913a17354bd1dc627d3c1639209640b9cac3fef5807"},
- {file = "coverage-7.5.3-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:341dd8f61c26337c37988345ca5c8ccabeff33093a26953a1ac72e7d0103c4fb"},
- {file = "coverage-7.5.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ab0b028165eea880af12f66086694768f2c3139b2c31ad5e032c8edbafca6ffc"},
- {file = "coverage-7.5.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:5bc5a8c87714b0c67cfeb4c7caa82b2d71e8864d1a46aa990b5588fa953673b8"},
- {file = "coverage-7.5.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:38a3b98dae8a7c9057bd91fbf3415c05e700a5114c5f1b5b0ea5f8f429ba6614"},
- {file = "coverage-7.5.3-cp311-cp311-win32.whl", hash = "sha256:fcf7d1d6f5da887ca04302db8e0e0cf56ce9a5e05f202720e49b3e8157ddb9a9"},
- {file = "coverage-7.5.3-cp311-cp311-win_amd64.whl", hash = "sha256:8c836309931839cca658a78a888dab9676b5c988d0dd34ca247f5f3e679f4e7a"},
- {file = "coverage-7.5.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:296a7d9bbc598e8744c00f7a6cecf1da9b30ae9ad51c566291ff1314e6cbbed8"},
- {file = "coverage-7.5.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:34d6d21d8795a97b14d503dcaf74226ae51eb1f2bd41015d3ef332a24d0a17b3"},
- {file = "coverage-7.5.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e317953bb4c074c06c798a11dbdd2cf9979dbcaa8ccc0fa4701d80042d4ebf1"},
- {file = "coverage-7.5.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:705f3d7c2b098c40f5b81790a5fedb274113373d4d1a69e65f8b68b0cc26f6db"},
- {file = "coverage-7.5.3-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b1196e13c45e327d6cd0b6e471530a1882f1017eb83c6229fc613cd1a11b53cd"},
- {file = "coverage-7.5.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:015eddc5ccd5364dcb902eaecf9515636806fa1e0d5bef5769d06d0f31b54523"},
- {file = "coverage-7.5.3-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:fd27d8b49e574e50caa65196d908f80e4dff64d7e592d0c59788b45aad7e8b35"},
- {file = "coverage-7.5.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:33fc65740267222fc02975c061eb7167185fef4cc8f2770267ee8bf7d6a42f84"},
- {file = "coverage-7.5.3-cp312-cp312-win32.whl", hash = "sha256:7b2a19e13dfb5c8e145c7a6ea959485ee8e2204699903c88c7d25283584bfc08"},
- {file = "coverage-7.5.3-cp312-cp312-win_amd64.whl", hash = "sha256:0bbddc54bbacfc09b3edaec644d4ac90c08ee8ed4844b0f86227dcda2d428fcb"},
- {file = "coverage-7.5.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:f78300789a708ac1f17e134593f577407d52d0417305435b134805c4fb135adb"},
- {file = "coverage-7.5.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b368e1aee1b9b75757942d44d7598dcd22a9dbb126affcbba82d15917f0cc155"},
- {file = "coverage-7.5.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f836c174c3a7f639bded48ec913f348c4761cbf49de4a20a956d3431a7c9cb24"},
- {file = "coverage-7.5.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:244f509f126dc71369393ce5fea17c0592c40ee44e607b6d855e9c4ac57aac98"},
- {file = "coverage-7.5.3-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c4c2872b3c91f9baa836147ca33650dc5c172e9273c808c3c3199c75490e709d"},
- {file = "coverage-7.5.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:dd4b3355b01273a56b20c219e74e7549e14370b31a4ffe42706a8cda91f19f6d"},
- {file = "coverage-7.5.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:f542287b1489c7a860d43a7d8883e27ca62ab84ca53c965d11dac1d3a1fab7ce"},
- {file = "coverage-7.5.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:75e3f4e86804023e991096b29e147e635f5e2568f77883a1e6eed74512659ab0"},
- {file = "coverage-7.5.3-cp38-cp38-win32.whl", hash = "sha256:c59d2ad092dc0551d9f79d9d44d005c945ba95832a6798f98f9216ede3d5f485"},
- {file = "coverage-7.5.3-cp38-cp38-win_amd64.whl", hash = "sha256:fa21a04112c59ad54f69d80e376f7f9d0f5f9123ab87ecd18fbb9ec3a2beed56"},
- {file = "coverage-7.5.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f5102a92855d518b0996eb197772f5ac2a527c0ec617124ad5242a3af5e25f85"},
- {file = "coverage-7.5.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:d1da0a2e3b37b745a2b2a678a4c796462cf753aebf94edcc87dcc6b8641eae31"},
- {file = "coverage-7.5.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8383a6c8cefba1b7cecc0149415046b6fc38836295bc4c84e820872eb5478b3d"},
- {file = "coverage-7.5.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9aad68c3f2566dfae84bf46295a79e79d904e1c21ccfc66de88cd446f8686341"},
- {file = "coverage-7.5.3-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2e079c9ec772fedbade9d7ebc36202a1d9ef7291bc9b3a024ca395c4d52853d7"},
- {file = "coverage-7.5.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:bde997cac85fcac227b27d4fb2c7608a2c5f6558469b0eb704c5726ae49e1c52"},
- {file = "coverage-7.5.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:990fb20b32990b2ce2c5f974c3e738c9358b2735bc05075d50a6f36721b8f303"},
- {file = "coverage-7.5.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:3d5a67f0da401e105753d474369ab034c7bae51a4c31c77d94030d59e41df5bd"},
- {file = "coverage-7.5.3-cp39-cp39-win32.whl", hash = "sha256:e08c470c2eb01977d221fd87495b44867a56d4d594f43739a8028f8646a51e0d"},
- {file = "coverage-7.5.3-cp39-cp39-win_amd64.whl", hash = "sha256:1d2a830ade66d3563bb61d1e3c77c8def97b30ed91e166c67d0632c018f380f0"},
- {file = "coverage-7.5.3-pp38.pp39.pp310-none-any.whl", hash = "sha256:3538d8fb1ee9bdd2e2692b3b18c22bb1c19ffbefd06880f5ac496e42d7bb3884"},
- {file = "coverage-7.5.3.tar.gz", hash = "sha256:04aefca5190d1dc7a53a4c1a5a7f8568811306d7a8ee231c42fb69215571944f"},
+ {file = "coverage-7.5.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6cfb5a4f556bb51aba274588200a46e4dd6b505fb1a5f8c5ae408222eb416f99"},
+ {file = "coverage-7.5.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2174e7c23e0a454ffe12267a10732c273243b4f2d50d07544a91198f05c48f47"},
+ {file = "coverage-7.5.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2214ee920787d85db1b6a0bd9da5f8503ccc8fcd5814d90796c2f2493a2f4d2e"},
+ {file = "coverage-7.5.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1137f46adb28e3813dec8c01fefadcb8c614f33576f672962e323b5128d9a68d"},
+ {file = "coverage-7.5.4-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b385d49609f8e9efc885790a5a0e89f2e3ae042cdf12958b6034cc442de428d3"},
+ {file = "coverage-7.5.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b4a474f799456e0eb46d78ab07303286a84a3140e9700b9e154cfebc8f527016"},
+ {file = "coverage-7.5.4-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:5cd64adedf3be66f8ccee418473c2916492d53cbafbfcff851cbec5a8454b136"},
+ {file = "coverage-7.5.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e564c2cf45d2f44a9da56f4e3a26b2236504a496eb4cb0ca7221cd4cc7a9aca9"},
+ {file = "coverage-7.5.4-cp310-cp310-win32.whl", hash = "sha256:7076b4b3a5f6d2b5d7f1185fde25b1e54eb66e647a1dfef0e2c2bfaf9b4c88c8"},
+ {file = "coverage-7.5.4-cp310-cp310-win_amd64.whl", hash = "sha256:018a12985185038a5b2bcafab04ab833a9a0f2c59995b3cec07e10074c78635f"},
+ {file = "coverage-7.5.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:db14f552ac38f10758ad14dd7b983dbab424e731588d300c7db25b6f89e335b5"},
+ {file = "coverage-7.5.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3257fdd8e574805f27bb5342b77bc65578e98cbc004a92232106344053f319ba"},
+ {file = "coverage-7.5.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3a6612c99081d8d6134005b1354191e103ec9705d7ba2754e848211ac8cacc6b"},
+ {file = "coverage-7.5.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d45d3cbd94159c468b9b8c5a556e3f6b81a8d1af2a92b77320e887c3e7a5d080"},
+ {file = "coverage-7.5.4-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ed550e7442f278af76d9d65af48069f1fb84c9f745ae249c1a183c1e9d1b025c"},
+ {file = "coverage-7.5.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7a892be37ca35eb5019ec85402c3371b0f7cda5ab5056023a7f13da0961e60da"},
+ {file = "coverage-7.5.4-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8192794d120167e2a64721d88dbd688584675e86e15d0569599257566dec9bf0"},
+ {file = "coverage-7.5.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:820bc841faa502e727a48311948e0461132a9c8baa42f6b2b84a29ced24cc078"},
+ {file = "coverage-7.5.4-cp311-cp311-win32.whl", hash = "sha256:6aae5cce399a0f065da65c7bb1e8abd5c7a3043da9dceb429ebe1b289bc07806"},
+ {file = "coverage-7.5.4-cp311-cp311-win_amd64.whl", hash = "sha256:d2e344d6adc8ef81c5a233d3a57b3c7d5181f40e79e05e1c143da143ccb6377d"},
+ {file = "coverage-7.5.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:54317c2b806354cbb2dc7ac27e2b93f97096912cc16b18289c5d4e44fc663233"},
+ {file = "coverage-7.5.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:042183de01f8b6d531e10c197f7f0315a61e8d805ab29c5f7b51a01d62782747"},
+ {file = "coverage-7.5.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a6bb74ed465d5fb204b2ec41d79bcd28afccf817de721e8a807d5141c3426638"},
+ {file = "coverage-7.5.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b3d45ff86efb129c599a3b287ae2e44c1e281ae0f9a9bad0edc202179bcc3a2e"},
+ {file = "coverage-7.5.4-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5013ed890dc917cef2c9f765c4c6a8ae9df983cd60dbb635df8ed9f4ebc9f555"},
+ {file = "coverage-7.5.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1014fbf665fef86cdfd6cb5b7371496ce35e4d2a00cda501cf9f5b9e6fced69f"},
+ {file = "coverage-7.5.4-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:3684bc2ff328f935981847082ba4fdc950d58906a40eafa93510d1b54c08a66c"},
+ {file = "coverage-7.5.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:581ea96f92bf71a5ec0974001f900db495488434a6928a2ca7f01eee20c23805"},
+ {file = "coverage-7.5.4-cp312-cp312-win32.whl", hash = "sha256:73ca8fbc5bc622e54627314c1a6f1dfdd8db69788f3443e752c215f29fa87a0b"},
+ {file = "coverage-7.5.4-cp312-cp312-win_amd64.whl", hash = "sha256:cef4649ec906ea7ea5e9e796e68b987f83fa9a718514fe147f538cfeda76d7a7"},
+ {file = "coverage-7.5.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:cdd31315fc20868c194130de9ee6bfd99755cc9565edff98ecc12585b90be882"},
+ {file = "coverage-7.5.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:02ff6e898197cc1e9fa375581382b72498eb2e6d5fc0b53f03e496cfee3fac6d"},
+ {file = "coverage-7.5.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d05c16cf4b4c2fc880cb12ba4c9b526e9e5d5bb1d81313d4d732a5b9fe2b9d53"},
+ {file = "coverage-7.5.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c5986ee7ea0795a4095ac4d113cbb3448601efca7f158ec7f7087a6c705304e4"},
+ {file = "coverage-7.5.4-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5df54843b88901fdc2f598ac06737f03d71168fd1175728054c8f5a2739ac3e4"},
+ {file = "coverage-7.5.4-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:ab73b35e8d109bffbda9a3e91c64e29fe26e03e49addf5b43d85fc426dde11f9"},
+ {file = "coverage-7.5.4-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:aea072a941b033813f5e4814541fc265a5c12ed9720daef11ca516aeacd3bd7f"},
+ {file = "coverage-7.5.4-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:16852febd96acd953b0d55fc842ce2dac1710f26729b31c80b940b9afcd9896f"},
+ {file = "coverage-7.5.4-cp38-cp38-win32.whl", hash = "sha256:8f894208794b164e6bd4bba61fc98bf6b06be4d390cf2daacfa6eca0a6d2bb4f"},
+ {file = "coverage-7.5.4-cp38-cp38-win_amd64.whl", hash = "sha256:e2afe743289273209c992075a5a4913e8d007d569a406ffed0bd080ea02b0633"},
+ {file = "coverage-7.5.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b95c3a8cb0463ba9f77383d0fa8c9194cf91f64445a63fc26fb2327e1e1eb088"},
+ {file = "coverage-7.5.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3d7564cc09dd91b5a6001754a5b3c6ecc4aba6323baf33a12bd751036c998be4"},
+ {file = "coverage-7.5.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:44da56a2589b684813f86d07597fdf8a9c6ce77f58976727329272f5a01f99f7"},
+ {file = "coverage-7.5.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e16f3d6b491c48c5ae726308e6ab1e18ee830b4cdd6913f2d7f77354b33f91c8"},
+ {file = "coverage-7.5.4-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dbc5958cb471e5a5af41b0ddaea96a37e74ed289535e8deca404811f6cb0bc3d"},
+ {file = "coverage-7.5.4-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:a04e990a2a41740b02d6182b498ee9796cf60eefe40cf859b016650147908029"},
+ {file = "coverage-7.5.4-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:ddbd2f9713a79e8e7242d7c51f1929611e991d855f414ca9996c20e44a895f7c"},
+ {file = "coverage-7.5.4-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:b1ccf5e728ccf83acd313c89f07c22d70d6c375a9c6f339233dcf792094bcbf7"},
+ {file = "coverage-7.5.4-cp39-cp39-win32.whl", hash = "sha256:56b4eafa21c6c175b3ede004ca12c653a88b6f922494b023aeb1e836df953ace"},
+ {file = "coverage-7.5.4-cp39-cp39-win_amd64.whl", hash = "sha256:65e528e2e921ba8fd67d9055e6b9f9e34b21ebd6768ae1c1723f4ea6ace1234d"},
+ {file = "coverage-7.5.4-pp38.pp39.pp310-none-any.whl", hash = "sha256:79b356f3dd5b26f3ad23b35c75dbdaf1f9e2450b6bcefc6d0825ea0aa3f86ca5"},
+ {file = "coverage-7.5.4.tar.gz", hash = "sha256:a44963520b069e12789d0faea4e9fdb1e410cdc4aab89d94f7f55cbb7fef0353"},
]
[package.dependencies]
@@ -782,43 +833,43 @@ toml = ["tomli"]
[[package]]
name = "cryptography"
-version = "42.0.7"
+version = "42.0.8"
description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers."
optional = false
python-versions = ">=3.7"
files = [
- {file = "cryptography-42.0.7-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:a987f840718078212fdf4504d0fd4c6effe34a7e4740378e59d47696e8dfb477"},
- {file = "cryptography-42.0.7-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:bd13b5e9b543532453de08bcdc3cc7cebec6f9883e886fd20a92f26940fd3e7a"},
- {file = "cryptography-42.0.7-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a79165431551042cc9d1d90e6145d5d0d3ab0f2d66326c201d9b0e7f5bf43604"},
- {file = "cryptography-42.0.7-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a47787a5e3649008a1102d3df55424e86606c9bae6fb77ac59afe06d234605f8"},
- {file = "cryptography-42.0.7-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:02c0eee2d7133bdbbc5e24441258d5d2244beb31da5ed19fbb80315f4bbbff55"},
- {file = "cryptography-42.0.7-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:5e44507bf8d14b36b8389b226665d597bc0f18ea035d75b4e53c7b1ea84583cc"},
- {file = "cryptography-42.0.7-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:7f8b25fa616d8b846aef64b15c606bb0828dbc35faf90566eb139aa9cff67af2"},
- {file = "cryptography-42.0.7-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:93a3209f6bb2b33e725ed08ee0991b92976dfdcf4e8b38646540674fc7508e13"},
- {file = "cryptography-42.0.7-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:e6b8f1881dac458c34778d0a424ae5769de30544fc678eac51c1c8bb2183e9da"},
- {file = "cryptography-42.0.7-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:3de9a45d3b2b7d8088c3fbf1ed4395dfeff79d07842217b38df14ef09ce1d8d7"},
- {file = "cryptography-42.0.7-cp37-abi3-win32.whl", hash = "sha256:789caea816c6704f63f6241a519bfa347f72fbd67ba28d04636b7c6b7da94b0b"},
- {file = "cryptography-42.0.7-cp37-abi3-win_amd64.whl", hash = "sha256:8cb8ce7c3347fcf9446f201dc30e2d5a3c898d009126010cbd1f443f28b52678"},
- {file = "cryptography-42.0.7-cp39-abi3-macosx_10_12_universal2.whl", hash = "sha256:a3a5ac8b56fe37f3125e5b72b61dcde43283e5370827f5233893d461b7360cd4"},
- {file = "cryptography-42.0.7-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:779245e13b9a6638df14641d029add5dc17edbef6ec915688f3acb9e720a5858"},
- {file = "cryptography-42.0.7-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0d563795db98b4cd57742a78a288cdbdc9daedac29f2239793071fe114f13785"},
- {file = "cryptography-42.0.7-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:31adb7d06fe4383226c3e963471f6837742889b3c4caa55aac20ad951bc8ffda"},
- {file = "cryptography-42.0.7-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:efd0bf5205240182e0f13bcaea41be4fdf5c22c5129fc7ced4a0282ac86998c9"},
- {file = "cryptography-42.0.7-cp39-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:a9bc127cdc4ecf87a5ea22a2556cab6c7eda2923f84e4f3cc588e8470ce4e42e"},
- {file = "cryptography-42.0.7-cp39-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:3577d029bc3f4827dd5bf8bf7710cac13527b470bbf1820a3f394adb38ed7d5f"},
- {file = "cryptography-42.0.7-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:2e47577f9b18723fa294b0ea9a17d5e53a227867a0a4904a1a076d1646d45ca1"},
- {file = "cryptography-42.0.7-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:1a58839984d9cb34c855197043eaae2c187d930ca6d644612843b4fe8513c886"},
- {file = "cryptography-42.0.7-cp39-abi3-win32.whl", hash = "sha256:e6b79d0adb01aae87e8a44c2b64bc3f3fe59515280e00fb6d57a7267a2583cda"},
- {file = "cryptography-42.0.7-cp39-abi3-win_amd64.whl", hash = "sha256:16268d46086bb8ad5bf0a2b5544d8a9ed87a0e33f5e77dd3c3301e63d941a83b"},
- {file = "cryptography-42.0.7-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:2954fccea107026512b15afb4aa664a5640cd0af630e2ee3962f2602693f0c82"},
- {file = "cryptography-42.0.7-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:362e7197754c231797ec45ee081f3088a27a47c6c01eff2ac83f60f85a50fe60"},
- {file = "cryptography-42.0.7-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:4f698edacf9c9e0371112792558d2f705b5645076cc0aaae02f816a0171770fd"},
- {file = "cryptography-42.0.7-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:5482e789294854c28237bba77c4c83be698be740e31a3ae5e879ee5444166582"},
- {file = "cryptography-42.0.7-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:e9b2a6309f14c0497f348d08a065d52f3020656f675819fc405fb63bbcd26562"},
- {file = "cryptography-42.0.7-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:d8e3098721b84392ee45af2dd554c947c32cc52f862b6a3ae982dbb90f577f14"},
- {file = "cryptography-42.0.7-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:c65f96dad14f8528a447414125e1fc8feb2ad5a272b8f68477abbcc1ea7d94b9"},
- {file = "cryptography-42.0.7-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:36017400817987670037fbb0324d71489b6ead6231c9604f8fc1f7d008087c68"},
- {file = "cryptography-42.0.7.tar.gz", hash = "sha256:ecbfbc00bf55888edda9868a4cf927205de8499e7fabe6c050322298382953f2"},
+ {file = "cryptography-42.0.8-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:81d8a521705787afe7a18d5bfb47ea9d9cc068206270aad0b96a725022e18d2e"},
+ {file = "cryptography-42.0.8-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:961e61cefdcb06e0c6d7e3a1b22ebe8b996eb2bf50614e89384be54c48c6b63d"},
+ {file = "cryptography-42.0.8-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e3ec3672626e1b9e55afd0df6d774ff0e953452886e06e0f1eb7eb0c832e8902"},
+ {file = "cryptography-42.0.8-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e599b53fd95357d92304510fb7bda8523ed1f79ca98dce2f43c115950aa78801"},
+ {file = "cryptography-42.0.8-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:5226d5d21ab681f432a9c1cf8b658c0cb02533eece706b155e5fbd8a0cdd3949"},
+ {file = "cryptography-42.0.8-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:6b7c4f03ce01afd3b76cf69a5455caa9cfa3de8c8f493e0d3ab7d20611c8dae9"},
+ {file = "cryptography-42.0.8-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:2346b911eb349ab547076f47f2e035fc8ff2c02380a7cbbf8d87114fa0f1c583"},
+ {file = "cryptography-42.0.8-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:ad803773e9df0b92e0a817d22fd8a3675493f690b96130a5e24f1b8fabbea9c7"},
+ {file = "cryptography-42.0.8-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:2f66d9cd9147ee495a8374a45ca445819f8929a3efcd2e3df6428e46c3cbb10b"},
+ {file = "cryptography-42.0.8-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:d45b940883a03e19e944456a558b67a41160e367a719833c53de6911cabba2b7"},
+ {file = "cryptography-42.0.8-cp37-abi3-win32.whl", hash = "sha256:a0c5b2b0585b6af82d7e385f55a8bc568abff8923af147ee3c07bd8b42cda8b2"},
+ {file = "cryptography-42.0.8-cp37-abi3-win_amd64.whl", hash = "sha256:57080dee41209e556a9a4ce60d229244f7a66ef52750f813bfbe18959770cfba"},
+ {file = "cryptography-42.0.8-cp39-abi3-macosx_10_12_universal2.whl", hash = "sha256:dea567d1b0e8bc5764b9443858b673b734100c2871dc93163f58c46a97a83d28"},
+ {file = "cryptography-42.0.8-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c4783183f7cb757b73b2ae9aed6599b96338eb957233c58ca8f49a49cc32fd5e"},
+ {file = "cryptography-42.0.8-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a0608251135d0e03111152e41f0cc2392d1e74e35703960d4190b2e0f4ca9c70"},
+ {file = "cryptography-42.0.8-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:dc0fdf6787f37b1c6b08e6dfc892d9d068b5bdb671198c72072828b80bd5fe4c"},
+ {file = "cryptography-42.0.8-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:9c0c1716c8447ee7dbf08d6db2e5c41c688544c61074b54fc4564196f55c25a7"},
+ {file = "cryptography-42.0.8-cp39-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:fff12c88a672ab9c9c1cf7b0c80e3ad9e2ebd9d828d955c126be4fd3e5578c9e"},
+ {file = "cryptography-42.0.8-cp39-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:cafb92b2bc622cd1aa6a1dce4b93307792633f4c5fe1f46c6b97cf67073ec961"},
+ {file = "cryptography-42.0.8-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:31f721658a29331f895a5a54e7e82075554ccfb8b163a18719d342f5ffe5ecb1"},
+ {file = "cryptography-42.0.8-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b297f90c5723d04bcc8265fc2a0f86d4ea2e0f7ab4b6994459548d3a6b992a14"},
+ {file = "cryptography-42.0.8-cp39-abi3-win32.whl", hash = "sha256:2f88d197e66c65be5e42cd72e5c18afbfae3f741742070e3019ac8f4ac57262c"},
+ {file = "cryptography-42.0.8-cp39-abi3-win_amd64.whl", hash = "sha256:fa76fbb7596cc5839320000cdd5d0955313696d9511debab7ee7278fc8b5c84a"},
+ {file = "cryptography-42.0.8-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:ba4f0a211697362e89ad822e667d8d340b4d8d55fae72cdd619389fb5912eefe"},
+ {file = "cryptography-42.0.8-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:81884c4d096c272f00aeb1f11cf62ccd39763581645b0812e99a91505fa48e0c"},
+ {file = "cryptography-42.0.8-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:c9bb2ae11bfbab395bdd072985abde58ea9860ed84e59dbc0463a5d0159f5b71"},
+ {file = "cryptography-42.0.8-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:7016f837e15b0a1c119d27ecd89b3515f01f90a8615ed5e9427e30d9cdbfed3d"},
+ {file = "cryptography-42.0.8-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5a94eccb2a81a309806027e1670a358b99b8fe8bfe9f8d329f27d72c094dde8c"},
+ {file = "cryptography-42.0.8-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:dec9b018df185f08483f294cae6ccac29e7a6e0678996587363dc352dc65c842"},
+ {file = "cryptography-42.0.8-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:343728aac38decfdeecf55ecab3264b015be68fc2816ca800db649607aeee648"},
+ {file = "cryptography-42.0.8-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:013629ae70b40af70c9a7a5db40abe5d9054e6f4380e50ce769947b73bf3caad"},
+ {file = "cryptography-42.0.8.tar.gz", hash = "sha256:8d09d05439ce7baa8e9e95b07ec5b6c886f548deb7e0f69ef25f64b3bce842f2"},
]
[package.dependencies]
@@ -891,71 +942,71 @@ serialization = ["protobuf (>=3.0.0)"]
[[package]]
name = "ddtrace"
-version = "2.9.0"
+version = "2.9.2"
description = "Datadog APM client library"
optional = false
python-versions = ">=3.7"
files = [
- {file = "ddtrace-2.9.0-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:95f6f23f935c3a4a43b02245067bf50f54057bebb32dd32fdcaa2db7affc4862"},
- {file = "ddtrace-2.9.0-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:4daa39ed9b9f8f932a386ed0d697767e9b982a34db0b2347c581f3b656dc8e90"},
- {file = "ddtrace-2.9.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da6f8e31104f673a5dd5b246c9b877be143cdac0a9428b262d4dba7df78349af"},
- {file = "ddtrace-2.9.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2e8caf09ec0fbd05bdc7a397680deeace7319fb047f4cf14a376c3e21d507d69"},
- {file = "ddtrace-2.9.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b01a4438f2d08b112641114bad7ead09fa31a450242db038f5c3c2973fdaab6f"},
- {file = "ddtrace-2.9.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:243b100d1db0e0652480d1cee4ee2b367846791b769421a275d5dda141b39df9"},
- {file = "ddtrace-2.9.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:cce137825e24e029978ad0556a360328098cee7e1e23bae91012f6765eb616f4"},
- {file = "ddtrace-2.9.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7b5fd5d4cc7fe035213179e3a98102ccf43e9baefd499d3a421658e22aacf20f"},
- {file = "ddtrace-2.9.0-cp310-cp310-win32.whl", hash = "sha256:a534604c03edcb899c03901845271953e7f32b98695400e7db59739c4f736dc5"},
- {file = "ddtrace-2.9.0-cp310-cp310-win_amd64.whl", hash = "sha256:07030249c095b67a63381a7b5a01a5321b6991df2677fe487a78f7703e3ed6d0"},
- {file = "ddtrace-2.9.0-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:e79a0f58ad413aaaa1a3249fae21a00cf4980250b2456bea55e2a5b1dc793c28"},
- {file = "ddtrace-2.9.0-cp311-cp311-macosx_11_0_x86_64.whl", hash = "sha256:75387aa906efde0a07dcb3ae27cf4b9cbe2d6480d5a5a4bec8199f7462b0d8a8"},
- {file = "ddtrace-2.9.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:622d0af63e28cb973bfd94809ced69b93c1560e2a30cddbd6091672c9d3e007a"},
- {file = "ddtrace-2.9.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2afb9e3623a6cd7a1757dd3771a9a8b7932ce088589929e9a97e18623cc7dae1"},
- {file = "ddtrace-2.9.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f22f65537cfbe43aeec9321d528924a99256f18141bd23afdb3aa19d71393acd"},
- {file = "ddtrace-2.9.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:53c1eca06c85d99d36cb0dcb7f0062a5b05ac4fbfbaffbc3c9d3982ceadef39b"},
- {file = "ddtrace-2.9.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:1d2f123002c47f4f52ee55faf4080d3dde86b85dd90e34b6dcbbe64958490ecd"},
- {file = "ddtrace-2.9.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:bf62180016d026689dc62244900409a5e111618a436c0231e46cf676fb3bbb11"},
- {file = "ddtrace-2.9.0-cp311-cp311-win32.whl", hash = "sha256:417b60d6cec637a027d448801ced48d41fe6e4ff48a835e2d06eae6b5255f0c4"},
- {file = "ddtrace-2.9.0-cp311-cp311-win_amd64.whl", hash = "sha256:cf14cb2e893734d8a9116d2d21ef6286d60e1a1f751bb5c3460c85b73bd73123"},
- {file = "ddtrace-2.9.0-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:ed1f636c7c151fee5a71b65b067d71e46789f980fc381728e693e0574ea9c1af"},
- {file = "ddtrace-2.9.0-cp312-cp312-macosx_11_0_x86_64.whl", hash = "sha256:e16b14b3644046a66764d97269f96ee359f0c9366bbca615ecd4cebdc21b612d"},
- {file = "ddtrace-2.9.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7625829a8bc6de5b2b01ab21c02d489a0f4bc166baae1c007786472c7a3b892d"},
- {file = "ddtrace-2.9.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:166292b189a90f9bf8affde627d844e5502a527903c5620a28c6713a7c8590cc"},
- {file = "ddtrace-2.9.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:08ab866972d485ebd5dd764a9264be67833ec70a845cf64478299ff61fb4f828"},
- {file = "ddtrace-2.9.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:6457c6262fd42d901e9c6f428ab950b8c8dba508512201efd1af92cbf42732a6"},
- {file = "ddtrace-2.9.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:d5d21ec52b3a0476e29407db9928dc59c97f0f16c6972eb893cfa351b3f9b93d"},
- {file = "ddtrace-2.9.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:6adf27d4e1b7d4253cabc9b766346d4e1f466997f7ac620e68edd2fd0f245e59"},
- {file = "ddtrace-2.9.0-cp312-cp312-win32.whl", hash = "sha256:dc9b6255c7d12db446b4e940f1cb5a98943a13d8ca98be7f47e611c10cca6596"},
- {file = "ddtrace-2.9.0-cp312-cp312-win_amd64.whl", hash = "sha256:3842f2039a631eb1fc8feefdb30d4bad8a74fdbf5b0df377d170b34aba8337a3"},
- {file = "ddtrace-2.9.0-cp37-cp37m-macosx_11_0_x86_64.whl", hash = "sha256:b40e8080cc22d82613bb14b21f51550f93ce5f6a664c70287f673ed827723016"},
- {file = "ddtrace-2.9.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:53d6635aef649650aebb8f870ce7eef472a837dcce9ada45dfbab2af4b9cb20a"},
- {file = "ddtrace-2.9.0-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:16e1ea6a507dd382b282430b00b33d819ca303143fafd02b494c90ae8531f849"},
- {file = "ddtrace-2.9.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc19958fd370b6c116a7729d2d2697ca150ded3951e6154a74755b6ca175d28a"},
- {file = "ddtrace-2.9.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:e99b2ad009d104fa58bdcaf35b69ade699101b93e267af1e1096602428e554e7"},
- {file = "ddtrace-2.9.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:b8422969326af5574435f96a2a4715d025e51e4b456f56879057679c741d3f38"},
- {file = "ddtrace-2.9.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:b46aa1da359e3b156e769b70df7441813f36b927f19d06885ab5b3ca4710c5a1"},
- {file = "ddtrace-2.9.0-cp37-cp37m-win32.whl", hash = "sha256:2adc40996fd4f88850d3356fcd2bf24db5b755ae0d9f5eeed36cbab31958f03d"},
- {file = "ddtrace-2.9.0-cp37-cp37m-win_amd64.whl", hash = "sha256:a2292449f9fca0fc9543f71cf31d55ecb201a34dff210e22159cc2746c133565"},
- {file = "ddtrace-2.9.0-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:d11895cbe71f140c4d220dcda06bb06c77d41e9101b4cd3c2f1d3ee3ae06b7cd"},
- {file = "ddtrace-2.9.0-cp38-cp38-macosx_11_0_x86_64.whl", hash = "sha256:3f1c36c4d819394639b9976fc2eecc2dab56bde960b8ca9f05ef936893eaa979"},
- {file = "ddtrace-2.9.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4bcb78811f8190f45ddc28e41f3c5dd2ea02aa4d0a2db5d75417b23ae9b35804"},
- {file = "ddtrace-2.9.0-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2b19f1a4c17c767701923365813d1ccf3f46ce456418b735699b139f3c4bdbca"},
- {file = "ddtrace-2.9.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a8e061570554051550e271126989c466471e810b65fcd7e9543fd4f56d567743"},
- {file = "ddtrace-2.9.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:4f912e21d4f2c0d1fed1a69e22282c02900bafbe1e8180f008534a837838de16"},
- {file = "ddtrace-2.9.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:aa502b336ecb7fc4aee0263392639c9f9f4dfef879667a2c341838263d151e02"},
- {file = "ddtrace-2.9.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:2ded8f9387e1967dd739e34ec874eddd42c4ad80c15c67050ed7fdffbbc88a3e"},
- {file = "ddtrace-2.9.0-cp38-cp38-win32.whl", hash = "sha256:82409c86fc12f7c7969e8538610eef8a74fc839d33e5a2ea64ad50011c296cbf"},
- {file = "ddtrace-2.9.0-cp38-cp38-win_amd64.whl", hash = "sha256:976188aadb18175810cb8ff966cb66a923cb3c4d6df5214b25ffb65369655ff5"},
- {file = "ddtrace-2.9.0-cp39-cp39-macosx_11_0_universal2.whl", hash = "sha256:c69f7cd19f1e170dde5601a38e9ed860cbf3bf81c447a2a56f8a9e7bbab48a14"},
- {file = "ddtrace-2.9.0-cp39-cp39-macosx_11_0_x86_64.whl", hash = "sha256:d5bbdc3ea83e6edb4553792b0513f5745b73d1be457338fd78b4e07ac4acfa19"},
- {file = "ddtrace-2.9.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e4147d0ad2996a18ad705873e4ae09c44b439d82fcfddb17a13a9f2f94555a4"},
- {file = "ddtrace-2.9.0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:835238b6488d85c36a41933201ee0a6e7e42eacf9ba4622b987ce0cd2edec18d"},
- {file = "ddtrace-2.9.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8b5227380ea379c8f0c8cdb6cfd98d432d16ff2e53a7141b335fb4d7a4ddc7d9"},
- {file = "ddtrace-2.9.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:fd41ca7ce56ff0f3eeeaee77fd9ab471554c58d0dfb68e6a10f10f756665b697"},
- {file = "ddtrace-2.9.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:ccc7f85ce0a3ba51da52f54f00bd156886e61162497eb9b940c0b8741026ba35"},
- {file = "ddtrace-2.9.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ecd5575dca41fe09f814badb3885ec0f70a241b3c18133da609be276b8a370e2"},
- {file = "ddtrace-2.9.0-cp39-cp39-win32.whl", hash = "sha256:ec6e49b389a60d183f472eeef308354962792137d9a5019895a8dd01925007fa"},
- {file = "ddtrace-2.9.0-cp39-cp39-win_amd64.whl", hash = "sha256:c9a7e267862e86f153e17f0ff347743aa13fad18d6efd1b40b0301623dfde70e"},
- {file = "ddtrace-2.9.0.tar.gz", hash = "sha256:f12fba84cfeb11a0a823e85e5b81ad56a2ea3a9de6a4f5a2c1a4038671afe521"},
+ {file = "ddtrace-2.9.2-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:aaa4c4c0d001e5695d8d8f03361e25fbba62716bd4dbc861daa45bc71802a165"},
+ {file = "ddtrace-2.9.2-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:99fa4f3437dd908622d015fd0a92015eb2bb718554fd6e9cb3c8984737ca8173"},
+ {file = "ddtrace-2.9.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6c3be4f5ea1378138f26b6a84b23048a681e705e602f5f4a2db6c9f1ae6f52c9"},
+ {file = "ddtrace-2.9.2-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b79f44ebd64496e8d2c85250290486f08cf338b02cb484a24d17204d11af39d6"},
+ {file = "ddtrace-2.9.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e0f9bcf9dc2fee145c1fa295e451898dd0b6fbdbdd7cc205b5c226c945369238"},
+ {file = "ddtrace-2.9.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:6418399be4eb0100d8c25e7154d94032dafb08f3387864db6ea64ae6b01044a4"},
+ {file = "ddtrace-2.9.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:72a3d3cdca7508b787e6bd0d09a75f1cb7cba9580f91591be51af22c9d9bf9bf"},
+ {file = "ddtrace-2.9.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:93efef2f0b88792fefe5840c47c9f262fc94471078c0cf10f54831b44ea422b6"},
+ {file = "ddtrace-2.9.2-cp310-cp310-win32.whl", hash = "sha256:5ad725a61da4b4d76368b7e205ae327ae39cab5ec64d8c6e16760bc86d6a6507"},
+ {file = "ddtrace-2.9.2-cp310-cp310-win_amd64.whl", hash = "sha256:9a31c9a2d714b3d45fb5ae24b912521d4569d1dac3fd3fc3c77ec9fcba5dfd26"},
+ {file = "ddtrace-2.9.2-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:358ccb1b7bf9ec39658e00aa1ba4972712603deefb5562219ce0ccc5e7521e52"},
+ {file = "ddtrace-2.9.2-cp311-cp311-macosx_11_0_x86_64.whl", hash = "sha256:98b44e28151b07a9ce8ae27951978ac340f66640c833dee9b396831ddf06a9a6"},
+ {file = "ddtrace-2.9.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c9d409e6d061bbe3d026696403edd37b390a4a8bc661b7490c02199a8a9da7e9"},
+ {file = "ddtrace-2.9.2-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6a26ecdf3f7666e604bb15e20d32b63d948e85bcde6c63b2f1d45af0681079bf"},
+ {file = "ddtrace-2.9.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:519e2a9e6daf592bf4a9993ae782621016770b5182ed7567fba0ef23812ca6d7"},
+ {file = "ddtrace-2.9.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:2243582de6aef14fc87621169d586679572cf3f39c79cef6f898963f37a6a296"},
+ {file = "ddtrace-2.9.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c176c0ea15e2b94f139ca68ba3d5ee48430c717ae785cd9e51eeb59634629c94"},
+ {file = "ddtrace-2.9.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e9befe7b40141a686f991fd98780b9dfe31e55b9ed3cf685a5fcfa256789b879"},
+ {file = "ddtrace-2.9.2-cp311-cp311-win32.whl", hash = "sha256:84f1a7b517f1790374ad1079e783cd893634518521ae6e2ed41a4e343227830b"},
+ {file = "ddtrace-2.9.2-cp311-cp311-win_amd64.whl", hash = "sha256:4c34823c3ed3e1da5fe11de483c4091179f21fb4f255144a5082af2f52a1e02e"},
+ {file = "ddtrace-2.9.2-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:b5b749b609036917cab6ae9187aaf4e83051e0396bd0d4d9f2af4bfbaf866bf2"},
+ {file = "ddtrace-2.9.2-cp312-cp312-macosx_11_0_x86_64.whl", hash = "sha256:b1f21076ecb3a7736e92dc288ff6437337098f9acc6dcbbebfcfb7a1ce7aabff"},
+ {file = "ddtrace-2.9.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8bb857a7a66ac56d041f2e40778f88cea51db55d0611beb36b6a45b52504c90d"},
+ {file = "ddtrace-2.9.2-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f1f7e403b77f6c26d2f813712c38cda09d4b5c2e07e5e6e578eb71ce674382ce"},
+ {file = "ddtrace-2.9.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dace336a9c14f6f8953732806d4fccee489d670aac6b2b75a3fa9eb94c32fda6"},
+ {file = "ddtrace-2.9.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:e7c59ce06f887db4a6a0309bdc504beada8969979876dc8f54681e10d1993426"},
+ {file = "ddtrace-2.9.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:0b7c81323a952da21e7a85b20334bb33ba47600c7b7604f4267022217c7025e3"},
+ {file = "ddtrace-2.9.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:07aa83c4a6e6822fd365a92f972576980badc3d426d257d9814212d0a2a5f837"},
+ {file = "ddtrace-2.9.2-cp312-cp312-win32.whl", hash = "sha256:5fe686fe657b9871f6faf2f7f7e97e659421c17dc5903b43ff174f8866726a21"},
+ {file = "ddtrace-2.9.2-cp312-cp312-win_amd64.whl", hash = "sha256:858b61e57cb11c5c467907add391ce8ad2dec823bc326c8e1505368c4f0ac7d4"},
+ {file = "ddtrace-2.9.2-cp37-cp37m-macosx_11_0_x86_64.whl", hash = "sha256:28ee6dee988609f1d720934d52f6a29b7c9b914a39fb70528a51a194d1ab3b8d"},
+ {file = "ddtrace-2.9.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:07ade55550c3b1debb96f9ffdb716eae5bd48335d3ca54e9c5b9e492a7dc91f2"},
+ {file = "ddtrace-2.9.2-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:41cf398da7a28a78c108cccfc87abe33d7e8936f99462f6fee3877fb180913c0"},
+ {file = "ddtrace-2.9.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2d79259e140070dc2533c3bc5776df7731baa9e2f078daf4ce708efd33ac00d3"},
+ {file = "ddtrace-2.9.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:753f845308d97f8290d4ff7ce92e7875b83efa4eb5ff3fac8e2042caf6761bfb"},
+ {file = "ddtrace-2.9.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:28c88f5efc946dddefc320c682c93bb65001ee38e78569e496b20823f21ef745"},
+ {file = "ddtrace-2.9.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:765d7c031b54da32fc18cbeafadd3c22cd1a6f98317e6e0498bf2898fbeae350"},
+ {file = "ddtrace-2.9.2-cp37-cp37m-win32.whl", hash = "sha256:7b5dddfbd23646a16ad9b991fd2866628dc56b7abe8dd7100962ce0681b738c9"},
+ {file = "ddtrace-2.9.2-cp37-cp37m-win_amd64.whl", hash = "sha256:c8c17f5f57f65ea95d6bf61511869abfaa10fb555e81b0294e30226afa047115"},
+ {file = "ddtrace-2.9.2-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:aa329ace4909bd402de3d9dbaaaff9e3545fd5a9fad1c72a39e075743c673099"},
+ {file = "ddtrace-2.9.2-cp38-cp38-macosx_11_0_x86_64.whl", hash = "sha256:afebe7e5467a743795a878cad3e9658f704c5572ca398a70a840da034a571f67"},
+ {file = "ddtrace-2.9.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bd1271c597f088b1ff7e5881138a1317a799025c834bd496cecfdcb816748e51"},
+ {file = "ddtrace-2.9.2-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:24da863c984d3261c7dae9362cf48c01fc0dc1557c92de336a1bbeb08452e046"},
+ {file = "ddtrace-2.9.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e68014fa46b4be6e58cc64feb3516ec856df714ce3d4576f3d6df9079ddfba8f"},
+ {file = "ddtrace-2.9.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:10824581a708c643515747e814c6b146bed6d91e687a825111858a198eee75e6"},
+ {file = "ddtrace-2.9.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:03c6874475db7d7b1fb563cd6aa3ba0c22ee72bb8c6cceb36c84dba6ca21e2f4"},
+ {file = "ddtrace-2.9.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:b91b8cfc7239317fe6b185beb0b8153769b43bf11fb2cda9e6e2996962e4b820"},
+ {file = "ddtrace-2.9.2-cp38-cp38-win32.whl", hash = "sha256:0d9456defb679d6225d32967902853cd4d8b01f55e4da18089a9ffa9d6495328"},
+ {file = "ddtrace-2.9.2-cp38-cp38-win_amd64.whl", hash = "sha256:d39c2da38c295aba1810083ce63d37041e3e40a06add960f6edf5a33517f743c"},
+ {file = "ddtrace-2.9.2-cp39-cp39-macosx_11_0_universal2.whl", hash = "sha256:c251e684c9e3a7828308a74d2be073d88cf28b4be457a5c201a2755ef9205d24"},
+ {file = "ddtrace-2.9.2-cp39-cp39-macosx_11_0_x86_64.whl", hash = "sha256:707151d2aa4f04388af4e3e8b0783e99b110fa0f2f1db775f64667c62bd249c2"},
+ {file = "ddtrace-2.9.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9c536a53d628e8d96812edea10d84e9df2f9022a7e932beb10e187c98f4471ec"},
+ {file = "ddtrace-2.9.2-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9c093fbabdeb6ecc6a749b1b5f80ebe557dcf768984bb42aadf66c57f04f3b85"},
+ {file = "ddtrace-2.9.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4d5b6c58d4ebc988f61a5f81e8953531ee59490240d69463592ff63dd2f6e00b"},
+ {file = "ddtrace-2.9.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:85511fade95b21ca29e9ba314eeb5847733a81128d8cbdbc43012caba45c03c8"},
+ {file = "ddtrace-2.9.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:8b3b2f64414c57742e7be924079e80576110abf8725f70e56bce0603877d08bf"},
+ {file = "ddtrace-2.9.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ad04028487d7cdb44318323ab4438c873e01855c3391a3c47a4400ff499bcbfc"},
+ {file = "ddtrace-2.9.2-cp39-cp39-win32.whl", hash = "sha256:206759c2847ee7174e14c4a2cffd3086ad55aca10d73f50b24cc2e00ec22e871"},
+ {file = "ddtrace-2.9.2-cp39-cp39-win_amd64.whl", hash = "sha256:d916dbfeeebb38cd48c64c771b74276b716902471f6bf9c02e8d5c278f0baad1"},
+ {file = "ddtrace-2.9.2.tar.gz", hash = "sha256:40775def3f3fc01d1c4c5eec64f7f624621eb394fe62d107c27e181123443716"},
]
[package.dependencies]
@@ -972,7 +1023,6 @@ opentelemetry-api = ">=1"
protobuf = ">=3"
setuptools = {version = "*", markers = "python_version >= \"3.12\""}
six = ">=1.12.0"
-sqlparse = ">=0.2.2"
typing-extensions = "*"
xmltodict = ">=0.12"
@@ -1037,6 +1087,17 @@ files = [
graph = ["objgraph (>=1.7.2)"]
profile = ["gprof2dot (>=2022.7.29)"]
+[[package]]
+name = "distlib"
+version = "0.3.8"
+description = "Distribution utilities"
+optional = false
+python-versions = "*"
+files = [
+ {file = "distlib-0.3.8-py2.py3-none-any.whl", hash = "sha256:034db59a0b96f8ca18035f36290806a9a6e6bd9d1ff91e45a7f172eb17e51784"},
+ {file = "distlib-0.3.8.tar.gz", hash = "sha256:1530ea13e350031b6312d8580ddb6b27a104275a31106523b8f123787f494f64"},
+]
+
[[package]]
name = "docker"
version = "7.1.0"
@@ -1103,13 +1164,13 @@ testing = ["hatch", "pre-commit", "pytest", "tox"]
[[package]]
name = "fastjsonschema"
-version = "2.19.1"
+version = "2.20.0"
description = "Fastest Python implementation of JSON schema"
optional = true
python-versions = "*"
files = [
- {file = "fastjsonschema-2.19.1-py3-none-any.whl", hash = "sha256:3672b47bc94178c9f23dbb654bf47440155d4db9df5f7bc47643315f9c405cd0"},
- {file = "fastjsonschema-2.19.1.tar.gz", hash = "sha256:e3126a94bdc4623d3de4485f8d468a12f02a67921315ddc87836d6e456dc789d"},
+ {file = "fastjsonschema-2.20.0-py3-none-any.whl", hash = "sha256:5875f0b0fa7a0043a91e93a9b8f793bcbbba9691e7fd83dca95c28ba26d21f0a"},
+ {file = "fastjsonschema-2.20.0.tar.gz", hash = "sha256:3d48fc5300ee96f5d116f10fe6f28d938e6008f59a6a025c2649475b87f76a23"},
]
[package.extras]
@@ -1117,18 +1178,18 @@ devel = ["colorama", "json-spec", "jsonschema", "pylint", "pytest", "pytest-benc
[[package]]
name = "filelock"
-version = "3.14.0"
+version = "3.15.4"
description = "A platform independent file lock."
optional = false
python-versions = ">=3.8"
files = [
- {file = "filelock-3.14.0-py3-none-any.whl", hash = "sha256:43339835842f110ca7ae60f1e1c160714c5a6afd15a2873419ab185334975c0f"},
- {file = "filelock-3.14.0.tar.gz", hash = "sha256:6ea72da3be9b8c82afd3edcf99f2fffbb5076335a5ae4d03248bb5b6c3eae78a"},
+ {file = "filelock-3.15.4-py3-none-any.whl", hash = "sha256:6ca1fffae96225dab4c6eaf1c4f4f28cd2568d3ec2a44e15a08520504de468e7"},
+ {file = "filelock-3.15.4.tar.gz", hash = "sha256:2207938cbc1844345cb01a5a95524dae30f0ce089eba5b00378295a17e3e90cb"},
]
[package.extras]
docs = ["furo (>=2023.9.10)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"]
-testing = ["covdefaults (>=2.3)", "coverage (>=7.3.2)", "diff-cover (>=8.0.1)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)", "pytest-timeout (>=2.2)"]
+testing = ["covdefaults (>=2.3)", "coverage (>=7.3.2)", "diff-cover (>=8.0.1)", "pytest (>=7.4.3)", "pytest-asyncio (>=0.21)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)", "pytest-timeout (>=2.2)", "virtualenv (>=20.26.2)"]
typing = ["typing-extensions (>=4.8)"]
[[package]]
@@ -1238,13 +1299,13 @@ socks = ["socksio (==1.*)"]
[[package]]
name = "hvac"
-version = "2.2.0"
+version = "2.3.0"
description = "HashiCorp Vault API client"
optional = false
python-versions = "<4.0,>=3.8"
files = [
- {file = "hvac-2.2.0-py3-none-any.whl", hash = "sha256:f287a19940c6fc518c723f8276cc9927f7400734303ee5872ac2e84539466d8d"},
- {file = "hvac-2.2.0.tar.gz", hash = "sha256:e4b0248c5672cb9a6f5974e7c8f5271a09c6c663cbf8ab11733a227f3d2db2c2"},
+ {file = "hvac-2.3.0-py3-none-any.whl", hash = "sha256:a3afc5710760b6ee9b3571769df87a0333da45da05a5f9f963e1d3925a84be7d"},
+ {file = "hvac-2.3.0.tar.gz", hash = "sha256:1b85e3320e8642dd82f234db63253cda169a817589e823713dc5fca83119b1e2"},
]
[package.dependencies]
@@ -1369,22 +1430,22 @@ files = [
[[package]]
name = "importlib-metadata"
-version = "7.1.0"
+version = "8.0.0"
description = "Read metadata from Python packages"
optional = false
python-versions = ">=3.8"
files = [
- {file = "importlib_metadata-7.1.0-py3-none-any.whl", hash = "sha256:30962b96c0c223483ed6cc7280e7f0199feb01a0e40cfae4d4450fc6fab1f570"},
- {file = "importlib_metadata-7.1.0.tar.gz", hash = "sha256:b78938b926ee8d5f020fc4772d487045805a55ddbad2ecf21c6d60938dc7fcd2"},
+ {file = "importlib_metadata-8.0.0-py3-none-any.whl", hash = "sha256:15584cf2b1bf449d98ff8a6ff1abef57bf20f3ac6454f431736cd3e660921b2f"},
+ {file = "importlib_metadata-8.0.0.tar.gz", hash = "sha256:188bd24e4c346d3f0a933f275c2fec67050326a856b9a359881d7c2a697e8812"},
]
[package.dependencies]
zipp = ">=0.5"
[package.extras]
-docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"]
+doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"]
perf = ["ipython"]
-testing = ["flufl.flake8", "importlib-resources (>=1.3)", "jaraco.test (>=5.4)", "packaging", "pyfakefs", "pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy", "pytest-perf (>=0.9.2)", "pytest-ruff (>=0.2.1)"]
+test = ["flufl.flake8", "importlib-resources (>=1.3)", "jaraco.test (>=5.4)", "packaging", "pyfakefs", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy", "pytest-perf (>=0.9.2)", "pytest-ruff (>=0.2.1)"]
[[package]]
name = "importlib-resources"
@@ -1457,31 +1518,15 @@ files = [
{file = "jmespath-1.0.1.tar.gz", hash = "sha256:90261b206d6defd58fdd5e85f478bf633a2901798906be2ad389150c5c60edbe"},
]
-[[package]]
-name = "jschema-to-python"
-version = "1.2.3"
-description = "Generate source code for Python classes from a JSON schema."
-optional = false
-python-versions = ">= 2.7"
-files = [
- {file = "jschema_to_python-1.2.3-py3-none-any.whl", hash = "sha256:8a703ca7604d42d74b2815eecf99a33359a8dccbb80806cce386d5e2dd992b05"},
- {file = "jschema_to_python-1.2.3.tar.gz", hash = "sha256:76ff14fe5d304708ccad1284e4b11f96a658949a31ee7faed9e0995279549b91"},
-]
-
-[package.dependencies]
-attrs = "*"
-jsonpickle = "*"
-pbr = "*"
-
[[package]]
name = "jsii"
-version = "1.99.0"
+version = "1.101.0"
description = "Python client for jsii runtime"
optional = false
python-versions = "~=3.8"
files = [
- {file = "jsii-1.99.0-py3-none-any.whl", hash = "sha256:e352232efb2c1994e882aab8c571c764055336b5c97c0f67ac654fbf3caa2411"},
- {file = "jsii-1.99.0.tar.gz", hash = "sha256:36e4c033d39b962c9d53910aa3e32cd16efa2109e4113fbd9d1be2e595350fe1"},
+ {file = "jsii-1.101.0-py3-none-any.whl", hash = "sha256:b78b87f8316560040ad0b9dca1682d73b6532a33acf4ecf56185d1ae5edb54fa"},
+ {file = "jsii-1.101.0.tar.gz", hash = "sha256:043c4d3d0d09af3c7265747f4da9c95770232477f75c846640df4c63d01b19cb"},
]
[package.dependencies]
@@ -1521,31 +1566,15 @@ files = [
[package.dependencies]
ply = "*"
-[[package]]
-name = "jsonpickle"
-version = "3.0.4"
-description = "Serialize any Python object to JSON"
-optional = false
-python-versions = ">=3.7"
-files = [
- {file = "jsonpickle-3.0.4-py3-none-any.whl", hash = "sha256:04ae7567a14269579e3af66b76bda284587458d7e8a204951ca8f71a3309952e"},
- {file = "jsonpickle-3.0.4.tar.gz", hash = "sha256:a1b14c8d6221cd8f394f2a97e735ea1d7edc927fbd135b26f2f8700657c8c62b"},
-]
-
-[package.extras]
-docs = ["furo", "rst.linker (>=1.9)", "sphinx"]
-packaging = ["build", "twine"]
-testing = ["bson", "ecdsa", "feedparser", "gmpy2", "numpy", "pandas", "pymongo", "pytest (>=3.5,!=3.7.3)", "pytest-benchmark", "pytest-benchmark[histogram]", "pytest-checkdocs (>=1.2.3)", "pytest-cov", "pytest-enabler (>=1.0.1)", "pytest-ruff (>=0.2.1)", "scikit-learn", "scipy", "scipy (>=1.9.3)", "simplejson", "sqlalchemy", "ujson"]
-
[[package]]
name = "jsonpointer"
-version = "2.4"
+version = "3.0.0"
description = "Identify specific nodes in a JSON document (RFC 6901)"
optional = false
-python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*, !=3.6.*"
+python-versions = ">=3.7"
files = [
- {file = "jsonpointer-2.4-py2.py3-none-any.whl", hash = "sha256:15d51bba20eea3165644553647711d150376234112651b4f1811022aecad7d7a"},
- {file = "jsonpointer-2.4.tar.gz", hash = "sha256:585cee82b70211fa9e6043b7bb89db6e1aa49524340dde8ad6b63206ea689d88"},
+ {file = "jsonpointer-3.0.0-py2.py3-none-any.whl", hash = "sha256:13e088adc14fca8b6aa8177c044e12701e6ad4b28ff10e65f2267a90109c9942"},
+ {file = "jsonpointer-3.0.0.tar.gz", hash = "sha256:2b2d729f2091522d61c3b31f82e11870f60b68f43fbc705cb76bf4b832af59ef"},
]
[[package]]
@@ -1586,20 +1615,6 @@ files = [
importlib-resources = {version = ">=1.4.0", markers = "python_version < \"3.9\""}
referencing = ">=0.31.0"
-[[package]]
-name = "junit-xml"
-version = "1.9"
-description = "Creates JUnit XML test result documents that can be read by tools such as Jenkins"
-optional = false
-python-versions = "*"
-files = [
- {file = "junit-xml-1.9.tar.gz", hash = "sha256:de16a051990d4e25a3982b2dd9e89d671067548718866416faec14d9de56db9f"},
- {file = "junit_xml-1.9-py2.py3-none-any.whl", hash = "sha256:ec5ca1a55aefdd76d28fcc0b135251d156c7106fa979686a4b48d62b761b4732"},
-]
-
-[package.dependencies]
-six = "*"
-
[[package]]
name = "mako"
version = "1.3.5"
@@ -1771,13 +1786,13 @@ files = [
[[package]]
name = "mike"
-version = "2.1.1"
+version = "2.1.2"
description = "Manage multiple versions of your MkDocs-powered documentation"
optional = false
python-versions = "*"
files = [
- {file = "mike-2.1.1-py3-none-any.whl", hash = "sha256:0b1d01a397a423284593eeb1b5f3194e37169488f929b860c9bfe95c0d5efb79"},
- {file = "mike-2.1.1.tar.gz", hash = "sha256:f39ed39f3737da83ad0adc33e9f885092ed27f8c9e7ff0523add0480352a2c22"},
+ {file = "mike-2.1.2-py3-none-any.whl", hash = "sha256:d61d9b423ab412d634ca2bd520136d5114e3cc73f4bbd1aa6a0c6625c04918c0"},
+ {file = "mike-2.1.2.tar.gz", hash = "sha256:d59cc8054c50f9c8a046cfd47f9b700cf9ff1b2b19f420bd8812ca6f94fa8bd3"},
]
[package.dependencies]
@@ -1859,13 +1874,13 @@ mkdocs = ">=0.17"
[[package]]
name = "mkdocs-material"
-version = "9.5.26"
+version = "9.5.27"
description = "Documentation that simply works"
optional = false
python-versions = ">=3.8"
files = [
- {file = "mkdocs_material-9.5.26-py3-none-any.whl", hash = "sha256:5d01fb0aa1c7946a1e3ae8689aa2b11a030621ecb54894e35aabb74c21016312"},
- {file = "mkdocs_material-9.5.26.tar.gz", hash = "sha256:56aeb91d94cffa43b6296fa4fbf0eb7c840136e563eecfd12c2d9e92e50ba326"},
+ {file = "mkdocs_material-9.5.27-py3-none-any.whl", hash = "sha256:af8cc263fafa98bb79e9e15a8c966204abf15164987569bd1175fd66a7705182"},
+ {file = "mkdocs_material-9.5.27.tar.gz", hash = "sha256:a7d4a35f6d4a62b0c43a0cfe7e987da0980c13587b5bc3c26e690ad494427ec0"},
]
[package.dependencies]
@@ -1940,38 +1955,38 @@ dill = ">=0.3.8"
[[package]]
name = "mypy"
-version = "1.10.0"
+version = "1.10.1"
description = "Optional static typing for Python"
optional = false
python-versions = ">=3.8"
files = [
- {file = "mypy-1.10.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:da1cbf08fb3b851ab3b9523a884c232774008267b1f83371ace57f412fe308c2"},
- {file = "mypy-1.10.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:12b6bfc1b1a66095ab413160a6e520e1dc076a28f3e22f7fb25ba3b000b4ef99"},
- {file = "mypy-1.10.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e36fb078cce9904c7989b9693e41cb9711e0600139ce3970c6ef814b6ebc2b2"},
- {file = "mypy-1.10.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:2b0695d605ddcd3eb2f736cd8b4e388288c21e7de85001e9f85df9187f2b50f9"},
- {file = "mypy-1.10.0-cp310-cp310-win_amd64.whl", hash = "sha256:cd777b780312ddb135bceb9bc8722a73ec95e042f911cc279e2ec3c667076051"},
- {file = "mypy-1.10.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3be66771aa5c97602f382230165b856c231d1277c511c9a8dd058be4784472e1"},
- {file = "mypy-1.10.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8b2cbaca148d0754a54d44121b5825ae71868c7592a53b7292eeb0f3fdae95ee"},
- {file = "mypy-1.10.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1ec404a7cbe9fc0e92cb0e67f55ce0c025014e26d33e54d9e506a0f2d07fe5de"},
- {file = "mypy-1.10.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e22e1527dc3d4aa94311d246b59e47f6455b8729f4968765ac1eacf9a4760bc7"},
- {file = "mypy-1.10.0-cp311-cp311-win_amd64.whl", hash = "sha256:a87dbfa85971e8d59c9cc1fcf534efe664d8949e4c0b6b44e8ca548e746a8d53"},
- {file = "mypy-1.10.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:a781f6ad4bab20eef8b65174a57e5203f4be627b46291f4589879bf4e257b97b"},
- {file = "mypy-1.10.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b808e12113505b97d9023b0b5e0c0705a90571c6feefc6f215c1df9381256e30"},
- {file = "mypy-1.10.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f55583b12156c399dce2df7d16f8a5095291354f1e839c252ec6c0611e86e2e"},
- {file = "mypy-1.10.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4cf18f9d0efa1b16478c4c129eabec36148032575391095f73cae2e722fcf9d5"},
- {file = "mypy-1.10.0-cp312-cp312-win_amd64.whl", hash = "sha256:bc6ac273b23c6b82da3bb25f4136c4fd42665f17f2cd850771cb600bdd2ebeda"},
- {file = "mypy-1.10.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:9fd50226364cd2737351c79807775136b0abe084433b55b2e29181a4c3c878c0"},
- {file = "mypy-1.10.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:f90cff89eea89273727d8783fef5d4a934be2fdca11b47def50cf5d311aff727"},
- {file = "mypy-1.10.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fcfc70599efde5c67862a07a1aaf50e55bce629ace26bb19dc17cece5dd31ca4"},
- {file = "mypy-1.10.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:075cbf81f3e134eadaf247de187bd604748171d6b79736fa9b6c9685b4083061"},
- {file = "mypy-1.10.0-cp38-cp38-win_amd64.whl", hash = "sha256:3f298531bca95ff615b6e9f2fc0333aae27fa48052903a0ac90215021cdcfa4f"},
- {file = "mypy-1.10.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:fa7ef5244615a2523b56c034becde4e9e3f9b034854c93639adb667ec9ec2976"},
- {file = "mypy-1.10.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3236a4c8f535a0631f85f5fcdffba71c7feeef76a6002fcba7c1a8e57c8be1ec"},
- {file = "mypy-1.10.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4a2b5cdbb5dd35aa08ea9114436e0d79aceb2f38e32c21684dcf8e24e1e92821"},
- {file = "mypy-1.10.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:92f93b21c0fe73dc00abf91022234c79d793318b8a96faac147cd579c1671746"},
- {file = "mypy-1.10.0-cp39-cp39-win_amd64.whl", hash = "sha256:28d0e038361b45f099cc086d9dd99c15ff14d0188f44ac883010e172ce86c38a"},
- {file = "mypy-1.10.0-py3-none-any.whl", hash = "sha256:f8c083976eb530019175aabadb60921e73b4f45736760826aa1689dda8208aee"},
- {file = "mypy-1.10.0.tar.gz", hash = "sha256:3d087fcbec056c4ee34974da493a826ce316947485cef3901f511848e687c131"},
+ {file = "mypy-1.10.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e36f229acfe250dc660790840916eb49726c928e8ce10fbdf90715090fe4ae02"},
+ {file = "mypy-1.10.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:51a46974340baaa4145363b9e051812a2446cf583dfaeba124af966fa44593f7"},
+ {file = "mypy-1.10.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:901c89c2d67bba57aaaca91ccdb659aa3a312de67f23b9dfb059727cce2e2e0a"},
+ {file = "mypy-1.10.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:0cd62192a4a32b77ceb31272d9e74d23cd88c8060c34d1d3622db3267679a5d9"},
+ {file = "mypy-1.10.1-cp310-cp310-win_amd64.whl", hash = "sha256:a2cbc68cb9e943ac0814c13e2452d2046c2f2b23ff0278e26599224cf164e78d"},
+ {file = "mypy-1.10.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:bd6f629b67bb43dc0d9211ee98b96d8dabc97b1ad38b9b25f5e4c4d7569a0c6a"},
+ {file = "mypy-1.10.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a1bbb3a6f5ff319d2b9d40b4080d46cd639abe3516d5a62c070cf0114a457d84"},
+ {file = "mypy-1.10.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b8edd4e9bbbc9d7b79502eb9592cab808585516ae1bcc1446eb9122656c6066f"},
+ {file = "mypy-1.10.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:6166a88b15f1759f94a46fa474c7b1b05d134b1b61fca627dd7335454cc9aa6b"},
+ {file = "mypy-1.10.1-cp311-cp311-win_amd64.whl", hash = "sha256:5bb9cd11c01c8606a9d0b83ffa91d0b236a0e91bc4126d9ba9ce62906ada868e"},
+ {file = "mypy-1.10.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:d8681909f7b44d0b7b86e653ca152d6dff0eb5eb41694e163c6092124f8246d7"},
+ {file = "mypy-1.10.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:378c03f53f10bbdd55ca94e46ec3ba255279706a6aacaecac52ad248f98205d3"},
+ {file = "mypy-1.10.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6bacf8f3a3d7d849f40ca6caea5c055122efe70e81480c8328ad29c55c69e93e"},
+ {file = "mypy-1.10.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:701b5f71413f1e9855566a34d6e9d12624e9e0a8818a5704d74d6b0402e66c04"},
+ {file = "mypy-1.10.1-cp312-cp312-win_amd64.whl", hash = "sha256:3c4c2992f6ea46ff7fce0072642cfb62af7a2484efe69017ed8b095f7b39ef31"},
+ {file = "mypy-1.10.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:604282c886497645ffb87b8f35a57ec773a4a2721161e709a4422c1636ddde5c"},
+ {file = "mypy-1.10.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:37fd87cab83f09842653f08de066ee68f1182b9b5282e4634cdb4b407266bade"},
+ {file = "mypy-1.10.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8addf6313777dbb92e9564c5d32ec122bf2c6c39d683ea64de6a1fd98b90fe37"},
+ {file = "mypy-1.10.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:5cc3ca0a244eb9a5249c7c583ad9a7e881aa5d7b73c35652296ddcdb33b2b9c7"},
+ {file = "mypy-1.10.1-cp38-cp38-win_amd64.whl", hash = "sha256:1b3a2ffce52cc4dbaeee4df762f20a2905aa171ef157b82192f2e2f368eec05d"},
+ {file = "mypy-1.10.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:fe85ed6836165d52ae8b88f99527d3d1b2362e0cb90b005409b8bed90e9059b3"},
+ {file = "mypy-1.10.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c2ae450d60d7d020d67ab440c6e3fae375809988119817214440033f26ddf7bf"},
+ {file = "mypy-1.10.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6be84c06e6abd72f960ba9a71561c14137a583093ffcf9bbfaf5e613d63fa531"},
+ {file = "mypy-1.10.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:2189ff1e39db399f08205e22a797383613ce1cb0cb3b13d8bcf0170e45b96cc3"},
+ {file = "mypy-1.10.1-cp39-cp39-win_amd64.whl", hash = "sha256:97a131ee36ac37ce9581f4220311247ab6cba896b4395b9c87af0675a13a755f"},
+ {file = "mypy-1.10.1-py3-none-any.whl", hash = "sha256:71d8ac0b906354ebda8ef1673e5fde785936ac1f29ff6987c7483cfbd5a4235a"},
+ {file = "mypy-1.10.1.tar.gz", hash = "sha256:1f8f492d7db9e3593ef42d4f115f04e556130f2819ad33ab84551403e97dd4c0"},
]
[package.dependencies]
@@ -2043,13 +2058,13 @@ typing-extensions = {version = ">=4.1.0", markers = "python_version < \"3.12\""}
[[package]]
name = "mypy-boto3-dynamodb"
-version = "1.34.114"
-description = "Type annotations for boto3.DynamoDB 1.34.114 service generated with mypy-boto3-builder 7.24.0"
+version = "1.34.131"
+description = "Type annotations for boto3.DynamoDB 1.34.131 service generated with mypy-boto3-builder 7.24.0"
optional = false
python-versions = ">=3.8"
files = [
- {file = "mypy_boto3_dynamodb-1.34.114-py3-none-any.whl", hash = "sha256:64be1fcd36db0daa354a78a2affdaef048653e4c5116da98f71446eee5db7638"},
- {file = "mypy_boto3_dynamodb-1.34.114.tar.gz", hash = "sha256:2a1a131587dbf857e5bec56ae84d8f9fb9618966e7a6120fb6c7da12cb73a82c"},
+ {file = "mypy_boto3_dynamodb-1.34.131-py3-none-any.whl", hash = "sha256:62e4fd85c621561f145828de14752a51380182d492cb039043d7f46bef872c34"},
+ {file = "mypy_boto3_dynamodb-1.34.131.tar.gz", hash = "sha256:d23c857568ae7c8c8fc1fbd96709a1dd00c140f917d74e09423fd44677097abf"},
]
[package.dependencies]
@@ -2099,13 +2114,13 @@ typing-extensions = {version = ">=4.1.0", markers = "python_version < \"3.12\""}
[[package]]
name = "mypy-boto3-secretsmanager"
-version = "1.34.109"
-description = "Type annotations for boto3.SecretsManager 1.34.109 service generated with mypy-boto3-builder 7.24.0"
+version = "1.34.128"
+description = "Type annotations for boto3.SecretsManager 1.34.128 service generated with mypy-boto3-builder 7.24.0"
optional = false
python-versions = ">=3.8"
files = [
- {file = "mypy_boto3_secretsmanager-1.34.109-py3-none-any.whl", hash = "sha256:18c60597a72ef08bad722f1c2f4507a0cf853c1526b1cffb8c3d2a30f5649d1f"},
- {file = "mypy_boto3_secretsmanager-1.34.109.tar.gz", hash = "sha256:29898fb1046fed5f83d05f08470d5cf07dfd1656b1da23f2bb875c9ff734ee65"},
+ {file = "mypy_boto3_secretsmanager-1.34.128-py3-none-any.whl", hash = "sha256:7ce9815d116fa1749971691355b1e1c8f462d46e7eaa9d84133b8db96dd3515f"},
+ {file = "mypy_boto3_secretsmanager-1.34.128.tar.gz", hash = "sha256:ae2b398efa1a32214c3eddb6901efa67cfc24a893b113f549a06bb70bb43b402"},
]
[package.dependencies]
@@ -2113,13 +2128,13 @@ typing-extensions = {version = ">=4.1.0", markers = "python_version < \"3.12\""}
[[package]]
name = "mypy-boto3-ssm"
-version = "1.34.91"
-description = "Type annotations for boto3.SSM 1.34.91 service generated with mypy-boto3-builder 7.23.2"
+version = "1.34.132"
+description = "Type annotations for boto3.SSM 1.34.132 service generated with mypy-boto3-builder 7.24.0"
optional = false
python-versions = ">=3.8"
files = [
- {file = "mypy_boto3_ssm-1.34.91-py3-none-any.whl", hash = "sha256:7ff84d33bdeb3b91bfc4af25a5d52ea8fa99f1e5c8247b33bdea51f174f492a8"},
- {file = "mypy_boto3_ssm-1.34.91.tar.gz", hash = "sha256:30097b0b2ead699e983f7faadd8b25b658c4cab02b21fcf67340b758ed45f874"},
+ {file = "mypy_boto3_ssm-1.34.132-py3-none-any.whl", hash = "sha256:c740e22b7e1c6d988e22a4d72ac36c4372a2e583ea81c3d9546c94e00b056394"},
+ {file = "mypy_boto3_ssm-1.34.132.tar.gz", hash = "sha256:6ef95781d9fe6d1d6ee51d7d9395b342adfa7ca7fdd43d7b2b5de96763f01239"},
]
[package.dependencies]
@@ -2168,6 +2183,28 @@ doc = ["nb2plots (>=0.6)", "numpydoc (>=1.5)", "pillow (>=9.4)", "pydata-sphinx-
extra = ["lxml (>=4.6)", "pydot (>=1.4.2)", "pygraphviz (>=1.10)", "sympy (>=1.10)"]
test = ["codecov (>=2.1)", "pytest (>=7.2)", "pytest-cov (>=4.0)"]
+[[package]]
+name = "nox"
+version = "2024.4.15"
+description = "Flexible test automation."
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "nox-2024.4.15-py3-none-any.whl", hash = "sha256:6492236efa15a460ecb98e7b67562a28b70da006ab0be164e8821177577c0565"},
+ {file = "nox-2024.4.15.tar.gz", hash = "sha256:ecf6700199cdfa9e5ea0a41ff5e6ef4641d09508eda6edb89d9987864115817f"},
+]
+
+[package.dependencies]
+argcomplete = ">=1.9.4,<4.0"
+colorlog = ">=2.6.1,<7.0.0"
+packaging = ">=20.9"
+tomli = {version = ">=1", markers = "python_version < \"3.11\""}
+virtualenv = ">=20.14.1"
+
+[package.extras]
+tox-to-nox = ["jinja2", "tox"]
+uv = ["uv (>=0.1.6)"]
+
[[package]]
name = "opentelemetry-api"
version = "1.16.0"
@@ -2185,13 +2222,13 @@ setuptools = ">=16.0"
[[package]]
name = "packaging"
-version = "24.0"
+version = "24.1"
description = "Core utilities for Python packages"
optional = false
-python-versions = ">=3.7"
+python-versions = ">=3.8"
files = [
- {file = "packaging-24.0-py3-none-any.whl", hash = "sha256:2ddfb553fdf02fb784c234c7ba6ccc288296ceabec964ad2eae3777778130bc5"},
- {file = "packaging-24.0.tar.gz", hash = "sha256:eb82c5e3e56209074766e6885bb04b8c38a0c015d0a30036ebe7ece34c9989e9"},
+ {file = "packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124"},
+ {file = "packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002"},
]
[[package]]
@@ -2228,13 +2265,12 @@ files = [
[[package]]
name = "pdoc3"
-version = "0.10.0"
+version = "0.11.0"
description = "Auto-generate API documentation for Python projects."
optional = false
-python-versions = ">= 3.6"
+python-versions = ">=3.7"
files = [
- {file = "pdoc3-0.10.0-py3-none-any.whl", hash = "sha256:ba45d1ada1bd987427d2bf5cdec30b2631a3ff5fb01f6d0e77648a572ce6028b"},
- {file = "pdoc3-0.10.0.tar.gz", hash = "sha256:5f22e7bcb969006738e1aa4219c75a32f34c2d62d46dc9d2fb2d3e0b0287e4b7"},
+ {file = "pdoc3-0.11.0.tar.gz", hash = "sha256:12f28c6ee045ca8ad6a624b86d1982c51de20e83c0a721cd7b0933f44ae0a655"},
]
[package.dependencies]
@@ -2296,22 +2332,22 @@ files = [
[[package]]
name = "protobuf"
-version = "5.27.0"
+version = "5.27.2"
description = ""
optional = false
python-versions = ">=3.8"
files = [
- {file = "protobuf-5.27.0-cp310-abi3-win32.whl", hash = "sha256:2f83bf341d925650d550b8932b71763321d782529ac0eaf278f5242f513cc04e"},
- {file = "protobuf-5.27.0-cp310-abi3-win_amd64.whl", hash = "sha256:b276e3f477ea1eebff3c2e1515136cfcff5ac14519c45f9b4aa2f6a87ea627c4"},
- {file = "protobuf-5.27.0-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:744489f77c29174328d32f8921566fb0f7080a2f064c5137b9d6f4b790f9e0c1"},
- {file = "protobuf-5.27.0-cp38-abi3-manylinux2014_aarch64.whl", hash = "sha256:f51f33d305e18646f03acfdb343aac15b8115235af98bc9f844bf9446573827b"},
- {file = "protobuf-5.27.0-cp38-abi3-manylinux2014_x86_64.whl", hash = "sha256:56937f97ae0dcf4e220ff2abb1456c51a334144c9960b23597f044ce99c29c89"},
- {file = "protobuf-5.27.0-cp38-cp38-win32.whl", hash = "sha256:a17f4d664ea868102feaa30a674542255f9f4bf835d943d588440d1f49a3ed15"},
- {file = "protobuf-5.27.0-cp38-cp38-win_amd64.whl", hash = "sha256:aabbbcf794fbb4c692ff14ce06780a66d04758435717107c387f12fb477bf0d8"},
- {file = "protobuf-5.27.0-cp39-cp39-win32.whl", hash = "sha256:587be23f1212da7a14a6c65fd61995f8ef35779d4aea9e36aad81f5f3b80aec5"},
- {file = "protobuf-5.27.0-cp39-cp39-win_amd64.whl", hash = "sha256:7cb65fc8fba680b27cf7a07678084c6e68ee13cab7cace734954c25a43da6d0f"},
- {file = "protobuf-5.27.0-py3-none-any.whl", hash = "sha256:673ad60f1536b394b4fa0bcd3146a4130fcad85bfe3b60eaa86d6a0ace0fa374"},
- {file = "protobuf-5.27.0.tar.gz", hash = "sha256:07f2b9a15255e3cf3f137d884af7972407b556a7a220912b252f26dc3121e6bf"},
+ {file = "protobuf-5.27.2-cp310-abi3-win32.whl", hash = "sha256:354d84fac2b0d76062e9b3221f4abbbacdfd2a4d8af36bab0474f3a0bb30ab38"},
+ {file = "protobuf-5.27.2-cp310-abi3-win_amd64.whl", hash = "sha256:0e341109c609749d501986b835f667c6e1e24531096cff9d34ae411595e26505"},
+ {file = "protobuf-5.27.2-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:a109916aaac42bff84702fb5187f3edadbc7c97fc2c99c5ff81dd15dcce0d1e5"},
+ {file = "protobuf-5.27.2-cp38-abi3-manylinux2014_aarch64.whl", hash = "sha256:176c12b1f1c880bf7a76d9f7c75822b6a2bc3db2d28baa4d300e8ce4cde7409b"},
+ {file = "protobuf-5.27.2-cp38-abi3-manylinux2014_x86_64.whl", hash = "sha256:b848dbe1d57ed7c191dfc4ea64b8b004a3f9ece4bf4d0d80a367b76df20bf36e"},
+ {file = "protobuf-5.27.2-cp38-cp38-win32.whl", hash = "sha256:4fadd8d83e1992eed0248bc50a4a6361dc31bcccc84388c54c86e530b7f58863"},
+ {file = "protobuf-5.27.2-cp38-cp38-win_amd64.whl", hash = "sha256:610e700f02469c4a997e58e328cac6f305f649826853813177e6290416e846c6"},
+ {file = "protobuf-5.27.2-cp39-cp39-win32.whl", hash = "sha256:9e8f199bf7f97bd7ecebffcae45ebf9527603549b2b562df0fbc6d4d688f14ca"},
+ {file = "protobuf-5.27.2-cp39-cp39-win_amd64.whl", hash = "sha256:7fc3add9e6003e026da5fc9e59b131b8f22b428b991ccd53e2af8071687b4fce"},
+ {file = "protobuf-5.27.2-py3-none-any.whl", hash = "sha256:54330f07e4949d09614707c48b06d1a22f8ffb5763c159efd5c0928326a91470"},
+ {file = "protobuf-5.27.2.tar.gz", hash = "sha256:f3ecdef226b9af856075f28227ff2c90ce3a594d092c39bee5513573f25e2714"},
]
[[package]]
@@ -2349,47 +2385,54 @@ files = [
[[package]]
name = "pydantic"
-version = "1.10.16"
+version = "1.10.17"
description = "Data validation and settings management using python type hints"
optional = false
python-versions = ">=3.7"
files = [
- {file = "pydantic-1.10.16-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1a539ac40551b01a85e899829aa43ca8036707474af8d74b48be288d4d2d2846"},
- {file = "pydantic-1.10.16-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8a4fcc7b0b8038dbda2dda642cff024032dfae24a7960cc58e57a39eb1949b9b"},
- {file = "pydantic-1.10.16-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4660dd697de1ae2d4305a85161312611f64d5360663a9ba026cd6ad9e3fe14c3"},
- {file = "pydantic-1.10.16-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:900a787c574f903a97d0bf52a43ff3b6cf4fa0119674bcfc0e5fd1056d388ad9"},
- {file = "pydantic-1.10.16-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:d30192a63e6d3334c3f0c0506dd6ae9f1dce7b2f8845518915291393a5707a22"},
- {file = "pydantic-1.10.16-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:16cf23ed599ca5ca937e37ba50ab114e6b5c387eb43a6cc533701605ad1be611"},
- {file = "pydantic-1.10.16-cp310-cp310-win_amd64.whl", hash = "sha256:8d23111f41d1e19334edd51438fd57933f3eee7d9d2fa8cc3f5eda515a272055"},
- {file = "pydantic-1.10.16-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ef287b8d7fc0e86a8bd1f902c61aff6ba9479c50563242fe88ba39692e98e1e0"},
- {file = "pydantic-1.10.16-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b9ded699bfd3b3912d796ff388b0c607e6d35d41053d37aaf8fd6082c660de9a"},
- {file = "pydantic-1.10.16-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:daeb199814333e4426c5e86d7fb610f4e230289f28cab90eb4de27330bef93cf"},
- {file = "pydantic-1.10.16-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5973843f1fa99ec6c3ac8d1a8698ac9340b35e45cca6c3e5beb5c3bd1ef15de6"},
- {file = "pydantic-1.10.16-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c6b8a7788a8528a558828fe4a48783cafdcf2612d13c491594a8161dc721629c"},
- {file = "pydantic-1.10.16-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:8abaecf54dacc9d991dda93c3b880d41092a8924cde94eeb811d7d9ab55df7d8"},
- {file = "pydantic-1.10.16-cp311-cp311-win_amd64.whl", hash = "sha256:ddc7b682fbd23f051edc419dc6977e11dd2dbdd0cef9d05f0e15d1387862d230"},
- {file = "pydantic-1.10.16-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:067c2b5539f7839653ad8c3d1fc2f1343338da8677b7b2172abf3cd3fdc8f719"},
- {file = "pydantic-1.10.16-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4d1fc943583c046ecad0ff5d6281ee571b64e11b5503d9595febdce54f38b290"},
- {file = "pydantic-1.10.16-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:18548b30ccebe71d380b0886cc44ea5d80afbcc155e3518792f13677ad06097d"},
- {file = "pydantic-1.10.16-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:4e92292f9580fc5ea517618580fac24e9f6dc5657196e977c194a8e50e14f5a9"},
- {file = "pydantic-1.10.16-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:5da8bc4bb4f85b8c97cc7f11141fddbbd29eb25e843672e5807e19cc3d7c1b7f"},
- {file = "pydantic-1.10.16-cp37-cp37m-win_amd64.whl", hash = "sha256:a04ee1ea34172b87707a6ecfcdb120d7656892206b7c4dbdb771a73e90179fcb"},
- {file = "pydantic-1.10.16-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:4fa86469fd46e732242c7acb83282d33f83591a7e06f840481327d5bf6d96112"},
- {file = "pydantic-1.10.16-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:89c2783dc261726fe7a5ce1121bce29a2f7eb9b1e704c68df2b117604e3b346f"},
- {file = "pydantic-1.10.16-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78e59fa919fa7a192f423d190d8660c35dd444efa9216662273f36826765424b"},
- {file = "pydantic-1.10.16-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b7e82a80068c77f4b074032e031e642530b6d45cb8121fc7c99faa31fb6c6b72"},
- {file = "pydantic-1.10.16-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:d82d5956cee27a30e26a5b88d00a6a2a15a4855e13c9baf50175976de0dc282c"},
- {file = "pydantic-1.10.16-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:4b7b99424cc0970ff08deccb549b5a6ec1040c0b449eab91723e64df2bd8fdca"},
- {file = "pydantic-1.10.16-cp38-cp38-win_amd64.whl", hash = "sha256:d97a35e1ba59442775201657171f601a2879e63517a55862a51f8d67cdfc0017"},
- {file = "pydantic-1.10.16-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9d91f6866fd3e303c632207813ef6bc4d86055e21c5e5a0a311983a9ac5f0192"},
- {file = "pydantic-1.10.16-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:d8d3c71d14c8bd26d2350c081908dbf59d5a6a8f9596d9ef2b09cc1e61c8662b"},
- {file = "pydantic-1.10.16-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b73e6386b439b4881d79244e9fc1e32d1e31e8d784673f5d58a000550c94a6c0"},
- {file = "pydantic-1.10.16-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5f039881fb2ef86f6de6eacce6e71701b47500355738367413ccc1550b2a69cf"},
- {file = "pydantic-1.10.16-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:3895ddb26f22bdddee7e49741486aa7b389258c6f6771943e87fc00eabd79134"},
- {file = "pydantic-1.10.16-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:55b945da2756b5cef93d792521ad0d457fdf2f69fd5a2d10a27513f5281717dd"},
- {file = "pydantic-1.10.16-cp39-cp39-win_amd64.whl", hash = "sha256:22dd265c77c3976a34be78409b128cb84629284dfd1b69d2fa1507a36f84dc8b"},
- {file = "pydantic-1.10.16-py3-none-any.whl", hash = "sha256:aa2774ba5412fd1c5cb890d08e8b0a3bb5765898913ba1f61a65a4810f03cf29"},
- {file = "pydantic-1.10.16.tar.gz", hash = "sha256:8bb388f6244809af69ee384900b10b677a69f1980fdc655ea419710cffcb5610"},
+ {file = "pydantic-1.10.17-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0fa51175313cc30097660b10eec8ca55ed08bfa07acbfe02f7a42f6c242e9a4b"},
+ {file = "pydantic-1.10.17-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c7e8988bb16988890c985bd2093df9dd731bfb9d5e0860db054c23034fab8f7a"},
+ {file = "pydantic-1.10.17-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:371dcf1831f87c9e217e2b6a0c66842879a14873114ebb9d0861ab22e3b5bb1e"},
+ {file = "pydantic-1.10.17-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4866a1579c0c3ca2c40575398a24d805d4db6cb353ee74df75ddeee3c657f9a7"},
+ {file = "pydantic-1.10.17-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:543da3c6914795b37785703ffc74ba4d660418620cc273490d42c53949eeeca6"},
+ {file = "pydantic-1.10.17-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7623b59876f49e61c2e283551cc3647616d2fbdc0b4d36d3d638aae8547ea681"},
+ {file = "pydantic-1.10.17-cp310-cp310-win_amd64.whl", hash = "sha256:409b2b36d7d7d19cd8310b97a4ce6b1755ef8bd45b9a2ec5ec2b124db0a0d8f3"},
+ {file = "pydantic-1.10.17-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:fa43f362b46741df8f201bf3e7dff3569fa92069bcc7b4a740dea3602e27ab7a"},
+ {file = "pydantic-1.10.17-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2a72d2a5ff86a3075ed81ca031eac86923d44bc5d42e719d585a8eb547bf0c9b"},
+ {file = "pydantic-1.10.17-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b4ad32aed3bf5eea5ca5decc3d1bbc3d0ec5d4fbcd72a03cdad849458decbc63"},
+ {file = "pydantic-1.10.17-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aeb4e741782e236ee7dc1fb11ad94dc56aabaf02d21df0e79e0c21fe07c95741"},
+ {file = "pydantic-1.10.17-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:d2f89a719411cb234105735a520b7c077158a81e0fe1cb05a79c01fc5eb59d3c"},
+ {file = "pydantic-1.10.17-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:db3b48d9283d80a314f7a682f7acae8422386de659fffaba454b77a083c3937d"},
+ {file = "pydantic-1.10.17-cp311-cp311-win_amd64.whl", hash = "sha256:9c803a5113cfab7bbb912f75faa4fc1e4acff43e452c82560349fff64f852e1b"},
+ {file = "pydantic-1.10.17-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:820ae12a390c9cbb26bb44913c87fa2ff431a029a785642c1ff11fed0a095fcb"},
+ {file = "pydantic-1.10.17-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c1e51d1af306641b7d1574d6d3307eaa10a4991542ca324f0feb134fee259815"},
+ {file = "pydantic-1.10.17-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e53fb834aae96e7b0dadd6e92c66e7dd9cdf08965340ed04c16813102a47fab"},
+ {file = "pydantic-1.10.17-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0e2495309b1266e81d259a570dd199916ff34f7f51f1b549a0d37a6d9b17b4dc"},
+ {file = "pydantic-1.10.17-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:098ad8de840c92ea586bf8efd9e2e90c6339d33ab5c1cfbb85be66e4ecf8213f"},
+ {file = "pydantic-1.10.17-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:525bbef620dac93c430d5d6bdbc91bdb5521698d434adf4434a7ef6ffd5c4b7f"},
+ {file = "pydantic-1.10.17-cp312-cp312-win_amd64.whl", hash = "sha256:6654028d1144df451e1da69a670083c27117d493f16cf83da81e1e50edce72ad"},
+ {file = "pydantic-1.10.17-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c87cedb4680d1614f1d59d13fea353faf3afd41ba5c906a266f3f2e8c245d655"},
+ {file = "pydantic-1.10.17-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:11289fa895bcbc8f18704efa1d8020bb9a86314da435348f59745473eb042e6b"},
+ {file = "pydantic-1.10.17-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:94833612d6fd18b57c359a127cbfd932d9150c1b72fea7c86ab58c2a77edd7c7"},
+ {file = "pydantic-1.10.17-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:d4ecb515fa7cb0e46e163ecd9d52f9147ba57bc3633dca0e586cdb7a232db9e3"},
+ {file = "pydantic-1.10.17-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:7017971ffa7fd7808146880aa41b266e06c1e6e12261768a28b8b41ba55c8076"},
+ {file = "pydantic-1.10.17-cp37-cp37m-win_amd64.whl", hash = "sha256:e840e6b2026920fc3f250ea8ebfdedf6ea7a25b77bf04c6576178e681942ae0f"},
+ {file = "pydantic-1.10.17-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:bfbb18b616abc4df70591b8c1ff1b3eabd234ddcddb86b7cac82657ab9017e33"},
+ {file = "pydantic-1.10.17-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:ebb249096d873593e014535ab07145498957091aa6ae92759a32d40cb9998e2e"},
+ {file = "pydantic-1.10.17-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d8c209af63ccd7b22fba94b9024e8b7fd07feffee0001efae50dd99316b27768"},
+ {file = "pydantic-1.10.17-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d4b40c9e13a0b61583e5599e7950490c700297b4a375b55b2b592774332798b7"},
+ {file = "pydantic-1.10.17-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:c31d281c7485223caf6474fc2b7cf21456289dbaa31401844069b77160cab9c7"},
+ {file = "pydantic-1.10.17-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:ae5184e99a060a5c80010a2d53c99aee76a3b0ad683d493e5f0620b5d86eeb75"},
+ {file = "pydantic-1.10.17-cp38-cp38-win_amd64.whl", hash = "sha256:ad1e33dc6b9787a6f0f3fd132859aa75626528b49cc1f9e429cdacb2608ad5f0"},
+ {file = "pydantic-1.10.17-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7e17c0ee7192e54a10943f245dc79e36d9fe282418ea05b886e1c666063a7b54"},
+ {file = "pydantic-1.10.17-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:cafb9c938f61d1b182dfc7d44a7021326547b7b9cf695db5b68ec7b590214773"},
+ {file = "pydantic-1.10.17-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95ef534e3c22e5abbdbdd6f66b6ea9dac3ca3e34c5c632894f8625d13d084cbe"},
+ {file = "pydantic-1.10.17-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:62d96b8799ae3d782df7ec9615cb59fc32c32e1ed6afa1b231b0595f6516e8ab"},
+ {file = "pydantic-1.10.17-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:ab2f976336808fd5d539fdc26eb51f9aafc1f4b638e212ef6b6f05e753c8011d"},
+ {file = "pydantic-1.10.17-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:b8ad363330557beac73159acfbeed220d5f1bfcd6b930302a987a375e02f74fd"},
+ {file = "pydantic-1.10.17-cp39-cp39-win_amd64.whl", hash = "sha256:48db882e48575ce4b39659558b2f9f37c25b8d348e37a2b4e32971dd5a7d6227"},
+ {file = "pydantic-1.10.17-py3-none-any.whl", hash = "sha256:e41b5b973e5c64f674b3b4720286ded184dcc26a691dd55f34391c62c6934688"},
+ {file = "pydantic-1.10.17.tar.gz", hash = "sha256:f434160fb14b353caf634149baaf847206406471ba70e64657c1e8330277a991"},
]
[package.dependencies]
@@ -2716,13 +2759,13 @@ toml = ["tomli (>=2.0.1)"]
[[package]]
name = "redis"
-version = "5.0.5"
+version = "5.0.7"
description = "Python client for Redis database and key-value store"
optional = false
python-versions = ">=3.7"
files = [
- {file = "redis-5.0.5-py3-none-any.whl", hash = "sha256:30b47d4ebb6b7a0b9b40c1275a19b87bb6f46b3bed82a89012cf56dea4024ada"},
- {file = "redis-5.0.5.tar.gz", hash = "sha256:3417688621acf6ee368dec4a04dd95881be24efd34c79f00d31f62bb528800ae"},
+ {file = "redis-5.0.7-py3-none-any.whl", hash = "sha256:0e479e24da960c690be5d9b96d21f7b918a98c0cf49af3b6fafaa0753f93a0db"},
+ {file = "redis-5.0.7.tar.gz", hash = "sha256:8f611490b93c8109b50adc317b31bfd84fff31def3475b92e7e80bf39f48175b"},
]
[package.dependencies]
@@ -2837,13 +2880,13 @@ files = [
[[package]]
name = "requests"
-version = "2.32.2"
+version = "2.32.3"
description = "Python HTTP for Humans."
optional = false
python-versions = ">=3.8"
files = [
- {file = "requests-2.32.2-py3-none-any.whl", hash = "sha256:fc06670dd0ed212426dfeb94fc1b983d917c4f9847c863f313c9dfaaffb7c23c"},
- {file = "requests-2.32.2.tar.gz", hash = "sha256:dd951ff5ecf3e3b3aa26b40703ba77495dab41da839ae72ef3c8e5d8e2433289"},
+ {file = "requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6"},
+ {file = "requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760"},
]
[package.dependencies]
@@ -2998,39 +3041,39 @@ files = [
[[package]]
name = "ruff"
-version = "0.4.8"
+version = "0.4.10"
description = "An extremely fast Python linter and code formatter, written in Rust."
optional = false
python-versions = ">=3.7"
files = [
- {file = "ruff-0.4.8-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:7663a6d78f6adb0eab270fa9cf1ff2d28618ca3a652b60f2a234d92b9ec89066"},
- {file = "ruff-0.4.8-py3-none-macosx_11_0_arm64.whl", hash = "sha256:eeceb78da8afb6de0ddada93112869852d04f1cd0f6b80fe464fd4e35c330913"},
- {file = "ruff-0.4.8-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aad360893e92486662ef3be0a339c5ca3c1b109e0134fcd37d534d4be9fb8de3"},
- {file = "ruff-0.4.8-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:284c2e3f3396fb05f5f803c9fffb53ebbe09a3ebe7dda2929ed8d73ded736deb"},
- {file = "ruff-0.4.8-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a7354f921e3fbe04d2a62d46707e569f9315e1a613307f7311a935743c51a764"},
- {file = "ruff-0.4.8-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:72584676164e15a68a15778fd1b17c28a519e7a0622161eb2debdcdabdc71883"},
- {file = "ruff-0.4.8-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9678d5c9b43315f323af2233a04d747409d1e3aa6789620083a82d1066a35199"},
- {file = "ruff-0.4.8-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704977a658131651a22b5ebeb28b717ef42ac6ee3b11e91dc87b633b5d83142b"},
- {file = "ruff-0.4.8-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d05f8d6f0c3cce5026cecd83b7a143dcad503045857bc49662f736437380ad45"},
- {file = "ruff-0.4.8-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:6ea874950daca5697309d976c9afba830d3bf0ed66887481d6bca1673fc5b66a"},
- {file = "ruff-0.4.8-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:fc95aac2943ddf360376be9aa3107c8cf9640083940a8c5bd824be692d2216dc"},
- {file = "ruff-0.4.8-py3-none-musllinux_1_2_i686.whl", hash = "sha256:384154a1c3f4bf537bac69f33720957ee49ac8d484bfc91720cc94172026ceed"},
- {file = "ruff-0.4.8-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:e9d5ce97cacc99878aa0d084c626a15cd21e6b3d53fd6f9112b7fc485918e1fa"},
- {file = "ruff-0.4.8-py3-none-win32.whl", hash = "sha256:6d795d7639212c2dfd01991259460101c22aabf420d9b943f153ab9d9706e6a9"},
- {file = "ruff-0.4.8-py3-none-win_amd64.whl", hash = "sha256:e14a3a095d07560a9d6769a72f781d73259655919d9b396c650fc98a8157555d"},
- {file = "ruff-0.4.8-py3-none-win_arm64.whl", hash = "sha256:14019a06dbe29b608f6b7cbcec300e3170a8d86efaddb7b23405cb7f7dcaf780"},
- {file = "ruff-0.4.8.tar.gz", hash = "sha256:16d717b1d57b2e2fd68bd0bf80fb43931b79d05a7131aa477d66fc40fbd86268"},
+ {file = "ruff-0.4.10-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:5c2c4d0859305ac5a16310eec40e4e9a9dec5dcdfbe92697acd99624e8638dac"},
+ {file = "ruff-0.4.10-py3-none-macosx_11_0_arm64.whl", hash = "sha256:a79489607d1495685cdd911a323a35871abfb7a95d4f98fc6f85e799227ac46e"},
+ {file = "ruff-0.4.10-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b1dd1681dfa90a41b8376a61af05cc4dc5ff32c8f14f5fe20dba9ff5deb80cd6"},
+ {file = "ruff-0.4.10-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c75c53bb79d71310dc79fb69eb4902fba804a81f374bc86a9b117a8d077a1784"},
+ {file = "ruff-0.4.10-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:18238c80ee3d9100d3535d8eb15a59c4a0753b45cc55f8bf38f38d6a597b9739"},
+ {file = "ruff-0.4.10-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:d8f71885bce242da344989cae08e263de29752f094233f932d4f5cfb4ef36a81"},
+ {file = "ruff-0.4.10-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:330421543bd3222cdfec481e8ff3460e8702ed1e58b494cf9d9e4bf90db52b9d"},
+ {file = "ruff-0.4.10-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9e9b6fb3a37b772628415b00c4fc892f97954275394ed611056a4b8a2631365e"},
+ {file = "ruff-0.4.10-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f54c481b39a762d48f64d97351048e842861c6662d63ec599f67d515cb417f6"},
+ {file = "ruff-0.4.10-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:67fe086b433b965c22de0b4259ddfe6fa541c95bf418499bedb9ad5fb8d1c631"},
+ {file = "ruff-0.4.10-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:acfaaab59543382085f9eb51f8e87bac26bf96b164839955f244d07125a982ef"},
+ {file = "ruff-0.4.10-py3-none-musllinux_1_2_i686.whl", hash = "sha256:3cea07079962b2941244191569cf3a05541477286f5cafea638cd3aa94b56815"},
+ {file = "ruff-0.4.10-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:338a64ef0748f8c3a80d7f05785930f7965d71ca260904a9321d13be24b79695"},
+ {file = "ruff-0.4.10-py3-none-win32.whl", hash = "sha256:ffe3cd2f89cb54561c62e5fa20e8f182c0a444934bf430515a4b422f1ab7b7ca"},
+ {file = "ruff-0.4.10-py3-none-win_amd64.whl", hash = "sha256:67f67cef43c55ffc8cc59e8e0b97e9e60b4837c8f21e8ab5ffd5d66e196e25f7"},
+ {file = "ruff-0.4.10-py3-none-win_arm64.whl", hash = "sha256:dd1fcee327c20addac7916ca4e2653fbbf2e8388d8a6477ce5b4e986b68ae6c0"},
+ {file = "ruff-0.4.10.tar.gz", hash = "sha256:3aa4f2bc388a30d346c56524f7cacca85945ba124945fe489952aadb6b5cd804"},
]
[[package]]
name = "s3transfer"
-version = "0.10.1"
+version = "0.10.2"
description = "An Amazon S3 Transfer Manager"
optional = false
-python-versions = ">= 3.8"
+python-versions = ">=3.8"
files = [
- {file = "s3transfer-0.10.1-py3-none-any.whl", hash = "sha256:ceb252b11bcf87080fb7850a224fb6e05c8a776bab8f2b64b7f25b969464839d"},
- {file = "s3transfer-0.10.1.tar.gz", hash = "sha256:5683916b4c724f799e600f41dd9e10a9ff19871bf87623cc8f491cb4f5fa0a19"},
+ {file = "s3transfer-0.10.2-py3-none-any.whl", hash = "sha256:eca1c20de70a39daee580aef4986996620f365c4e0fda6a86100231d62f1bf69"},
+ {file = "s3transfer-0.10.2.tar.gz", hash = "sha256:0711534e9356d3cc692fdde846b4a1e4b0cb6519971860796e6bc4c7aea00ef6"},
]
[package.dependencies]
@@ -3039,30 +3082,15 @@ botocore = ">=1.33.2,<2.0a.0"
[package.extras]
crt = ["botocore[crt] (>=1.33.2,<2.0a.0)"]
-[[package]]
-name = "sarif-om"
-version = "1.0.4"
-description = "Classes implementing the SARIF 2.1.0 object model."
-optional = false
-python-versions = ">= 2.7"
-files = [
- {file = "sarif_om-1.0.4-py3-none-any.whl", hash = "sha256:539ef47a662329b1c8502388ad92457425e95dc0aaaf995fe46f4984c4771911"},
- {file = "sarif_om-1.0.4.tar.gz", hash = "sha256:cd5f416b3083e00d402a92e449a7ff67af46f11241073eea0461802a3b5aef98"},
-]
-
-[package.dependencies]
-attrs = "*"
-pbr = "*"
-
[[package]]
name = "sentry-sdk"
-version = "2.5.1"
+version = "2.7.0"
description = "Python client for Sentry (https://sentry.io)"
optional = false
python-versions = ">=3.6"
files = [
- {file = "sentry_sdk-2.5.1-py2.py3-none-any.whl", hash = "sha256:1f87acdce4a43a523ae5aa21a3fc37522d73ebd9ec04b1dbf01aa3d173852def"},
- {file = "sentry_sdk-2.5.1.tar.gz", hash = "sha256:fbc40a78a8a9c6675133031116144f0d0940376fa6e4e1acd5624c90b0aaf58b"},
+ {file = "sentry_sdk-2.7.0-py2.py3-none-any.whl", hash = "sha256:db9594c27a4d21c1ebad09908b1f0dc808ef65c2b89c1c8e7e455143262e37c1"},
+ {file = "sentry_sdk-2.7.0.tar.gz", hash = "sha256:d846a211d4a0378b289ced3c434480945f110d0ede00450ba631fc2852e7a0d4"},
]
[package.dependencies]
@@ -3092,7 +3120,7 @@ langchain = ["langchain (>=0.0.210)"]
loguru = ["loguru (>=0.5)"]
openai = ["openai (>=1.0.0)", "tiktoken (>=0.3.0)"]
opentelemetry = ["opentelemetry-distro (>=0.35b0)"]
-opentelemetry-experimental = ["opentelemetry-distro (>=0.40b0,<1.0)", "opentelemetry-instrumentation-aiohttp-client (>=0.40b0,<1.0)", "opentelemetry-instrumentation-django (>=0.40b0,<1.0)", "opentelemetry-instrumentation-fastapi (>=0.40b0,<1.0)", "opentelemetry-instrumentation-flask (>=0.40b0,<1.0)", "opentelemetry-instrumentation-requests (>=0.40b0,<1.0)", "opentelemetry-instrumentation-sqlite3 (>=0.40b0,<1.0)", "opentelemetry-instrumentation-urllib (>=0.40b0,<1.0)"]
+opentelemetry-experimental = ["opentelemetry-instrumentation-aio-pika (==0.46b0)", "opentelemetry-instrumentation-aiohttp-client (==0.46b0)", "opentelemetry-instrumentation-aiopg (==0.46b0)", "opentelemetry-instrumentation-asgi (==0.46b0)", "opentelemetry-instrumentation-asyncio (==0.46b0)", "opentelemetry-instrumentation-asyncpg (==0.46b0)", "opentelemetry-instrumentation-aws-lambda (==0.46b0)", "opentelemetry-instrumentation-boto (==0.46b0)", "opentelemetry-instrumentation-boto3sqs (==0.46b0)", "opentelemetry-instrumentation-botocore (==0.46b0)", "opentelemetry-instrumentation-cassandra (==0.46b0)", "opentelemetry-instrumentation-celery (==0.46b0)", "opentelemetry-instrumentation-confluent-kafka (==0.46b0)", "opentelemetry-instrumentation-dbapi (==0.46b0)", "opentelemetry-instrumentation-django (==0.46b0)", "opentelemetry-instrumentation-elasticsearch (==0.46b0)", "opentelemetry-instrumentation-falcon (==0.46b0)", "opentelemetry-instrumentation-fastapi (==0.46b0)", "opentelemetry-instrumentation-flask (==0.46b0)", "opentelemetry-instrumentation-grpc (==0.46b0)", "opentelemetry-instrumentation-httpx (==0.46b0)", "opentelemetry-instrumentation-jinja2 (==0.46b0)", "opentelemetry-instrumentation-kafka-python (==0.46b0)", "opentelemetry-instrumentation-logging (==0.46b0)", "opentelemetry-instrumentation-mysql (==0.46b0)", "opentelemetry-instrumentation-mysqlclient (==0.46b0)", "opentelemetry-instrumentation-pika (==0.46b0)", "opentelemetry-instrumentation-psycopg (==0.46b0)", "opentelemetry-instrumentation-psycopg2 (==0.46b0)", "opentelemetry-instrumentation-pymemcache (==0.46b0)", "opentelemetry-instrumentation-pymongo (==0.46b0)", "opentelemetry-instrumentation-pymysql (==0.46b0)", "opentelemetry-instrumentation-pyramid (==0.46b0)", "opentelemetry-instrumentation-redis (==0.46b0)", "opentelemetry-instrumentation-remoulade (==0.46b0)", "opentelemetry-instrumentation-requests (==0.46b0)", "opentelemetry-instrumentation-sklearn (==0.46b0)", "opentelemetry-instrumentation-sqlalchemy (==0.46b0)", "opentelemetry-instrumentation-sqlite3 (==0.46b0)", "opentelemetry-instrumentation-starlette (==0.46b0)", "opentelemetry-instrumentation-system-metrics (==0.46b0)", "opentelemetry-instrumentation-threading (==0.46b0)", "opentelemetry-instrumentation-tornado (==0.46b0)", "opentelemetry-instrumentation-tortoiseorm (==0.46b0)", "opentelemetry-instrumentation-urllib (==0.46b0)", "opentelemetry-instrumentation-urllib3 (==0.46b0)", "opentelemetry-instrumentation-wsgi (==0.46b0)"]
pure-eval = ["asttokens", "executing", "pure-eval"]
pymongo = ["pymongo (>=3.1)"]
pyspark = ["pyspark (>=2.4.4)"]
@@ -3106,18 +3134,18 @@ tornado = ["tornado (>=5)"]
[[package]]
name = "setuptools"
-version = "70.0.0"
+version = "70.1.1"
description = "Easily download, build, install, upgrade, and uninstall Python packages"
optional = false
python-versions = ">=3.8"
files = [
- {file = "setuptools-70.0.0-py3-none-any.whl", hash = "sha256:54faa7f2e8d2d11bcd2c07bed282eef1046b5c080d1c32add737d7b5817b1ad4"},
- {file = "setuptools-70.0.0.tar.gz", hash = "sha256:f211a66637b8fa059bb28183da127d4e86396c991a942b028c6650d4319c3fd0"},
+ {file = "setuptools-70.1.1-py3-none-any.whl", hash = "sha256:a58a8fde0541dab0419750bcc521fbdf8585f6e5cb41909df3a472ef7b81ca95"},
+ {file = "setuptools-70.1.1.tar.gz", hash = "sha256:937a48c7cdb7a21eb53cd7f9b59e525503aa8abaf3584c730dc5f7a5bec3a650"},
]
[package.extras]
docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "pyproject-hooks (!=1.1)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"]
-testing = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "importlib-metadata", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "mypy (==1.9)", "packaging (>=23.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.1)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-home (>=0.5)", "pytest-mypy", "pytest-perf", "pytest-ruff (>=0.2.1)", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"]
+testing = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "importlib-metadata", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "jaraco.test", "mypy (==1.10.0)", "packaging (>=23.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.1)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-home (>=0.5)", "pytest-mypy", "pytest-perf", "pytest-ruff (>=0.3.2)", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"]
[[package]]
name = "six"
@@ -3152,21 +3180,6 @@ files = [
{file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"},
]
-[[package]]
-name = "sqlparse"
-version = "0.5.0"
-description = "A non-validating SQL parser."
-optional = false
-python-versions = ">=3.8"
-files = [
- {file = "sqlparse-0.5.0-py3-none-any.whl", hash = "sha256:c204494cd97479d0e39f28c93d46c0b2d5959c7b9ab904762ea6c7af211c8663"},
- {file = "sqlparse-0.5.0.tar.gz", hash = "sha256:714d0a4932c059d16189f58ef5411ec2287a4360f17cdd0edd2d09d4c5087c93"},
-]
-
-[package.extras]
-dev = ["build", "hatch"]
-doc = ["sphinx"]
-
[[package]]
name = "stevedore"
version = "5.2.0"
@@ -3183,17 +3196,17 @@ pbr = ">=2.0.0,<2.1.0 || >2.1.0"
[[package]]
name = "sympy"
-version = "1.12"
+version = "1.12.1"
description = "Computer algebra system (CAS) in Python"
optional = false
python-versions = ">=3.8"
files = [
- {file = "sympy-1.12-py3-none-any.whl", hash = "sha256:c3588cd4295d0c0f603d0f2ae780587e64e2efeedb3521e46b9bb1d08d184fa5"},
- {file = "sympy-1.12.tar.gz", hash = "sha256:ebf595c8dac3e0fdc4152c51878b498396ec7f30e7a914d6071e674d49420fb8"},
+ {file = "sympy-1.12.1-py3-none-any.whl", hash = "sha256:9b2cbc7f1a640289430e13d2a56f02f867a1da0190f2f99d8968c2f74da0e515"},
+ {file = "sympy-1.12.1.tar.gz", hash = "sha256:2877b03f998cd8c08f07cd0de5b767119cd3ef40d09f41c30d722f6686b0fb88"},
]
[package.dependencies]
-mpmath = ">=0.19"
+mpmath = ">=1.1.0,<1.4.0"
[[package]]
name = "testcontainers"
@@ -3324,15 +3337,29 @@ files = [
[package.dependencies]
types-urllib3 = "*"
+[[package]]
+name = "types-requests"
+version = "2.32.0.20240622"
+description = "Typing stubs for requests"
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "types-requests-2.32.0.20240622.tar.gz", hash = "sha256:ed5e8a412fcc39159d6319385c009d642845f250c63902718f605cd90faade31"},
+ {file = "types_requests-2.32.0.20240622-py3-none-any.whl", hash = "sha256:97bac6b54b5bd4cf91d407e62f0932a74821bc2211f22116d9ee1dd643826caf"},
+]
+
+[package.dependencies]
+urllib3 = ">=2"
+
[[package]]
name = "types-setuptools"
-version = "70.0.0.20240524"
+version = "70.1.0.20240627"
description = "Typing stubs for setuptools"
optional = false
python-versions = ">=3.8"
files = [
- {file = "types-setuptools-70.0.0.20240524.tar.gz", hash = "sha256:e31fee7b9d15ef53980526579ac6089b3ae51a005a281acf97178e90ac71aff6"},
- {file = "types_setuptools-70.0.0.20240524-py3-none-any.whl", hash = "sha256:8f5379b9948682d72a9ab531fbe52932e84c4f38deda570255f9bae3edd766bc"},
+ {file = "types-setuptools-70.1.0.20240627.tar.gz", hash = "sha256:385907a47b5cf302b928ce07953cd91147d5de6f3da604c31905fdf0ec309e83"},
+ {file = "types_setuptools-70.1.0.20240627-py3-none-any.whl", hash = "sha256:c7bdf05cd0a8b66868b4774c7b3c079d01ae025d8c9562bfc8bf2ff44d263c9c"},
]
[[package]]
@@ -3446,13 +3473,13 @@ files = [
[[package]]
name = "urllib3"
-version = "1.26.18"
+version = "1.26.19"
description = "HTTP library with thread-safe connection pooling, file post, and more."
optional = false
-python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*"
+python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7"
files = [
- {file = "urllib3-1.26.18-py2.py3-none-any.whl", hash = "sha256:34b97092d7e0a3a8cf7cd10e386f401b3737364026c45e622aa02903dffe0f07"},
- {file = "urllib3-1.26.18.tar.gz", hash = "sha256:f8ecc1bba5667413457c529ab955bf8c67b45db799d159066261719e328580a0"},
+ {file = "urllib3-1.26.19-py2.py3-none-any.whl", hash = "sha256:37a0344459b199fce0e80b0d3569837ec6b6937435c5244e7fd73fa6006830f3"},
+ {file = "urllib3-1.26.19.tar.gz", hash = "sha256:3e3d753a8618b86d7de333b4223005f68720bcd6a7d2bcb9fbd2229ec7c1e429"},
]
[package.extras]
@@ -3491,6 +3518,26 @@ files = [
[package.extras]
test = ["coverage", "flake8 (>=3.7)", "mypy", "pretend", "pytest"]
+[[package]]
+name = "virtualenv"
+version = "20.26.3"
+description = "Virtual Python Environment builder"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "virtualenv-20.26.3-py3-none-any.whl", hash = "sha256:8cc4a31139e796e9a7de2cd5cf2489de1217193116a8fd42328f1bd65f434589"},
+ {file = "virtualenv-20.26.3.tar.gz", hash = "sha256:4c43a2a236279d9ea36a0d76f98d84bd6ca94ac4e0f4a3b9d46d05e10fea542a"},
+]
+
+[package.dependencies]
+distlib = ">=0.3.7,<1"
+filelock = ">=3.12.2,<4"
+platformdirs = ">=3.9.1,<5"
+
+[package.extras]
+docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2,!=7.3)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"]
+test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8)", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10)"]
+
[[package]]
name = "watchdog"
version = "4.0.1"
@@ -3643,18 +3690,18 @@ files = [
[[package]]
name = "zipp"
-version = "3.19.0"
+version = "3.19.2"
description = "Backport of pathlib-compatible object wrapper for zip files"
optional = false
python-versions = ">=3.8"
files = [
- {file = "zipp-3.19.0-py3-none-any.whl", hash = "sha256:96dc6ad62f1441bcaccef23b274ec471518daf4fbbc580341204936a5a3dddec"},
- {file = "zipp-3.19.0.tar.gz", hash = "sha256:952df858fb3164426c976d9338d3961e8e8b3758e2e059e0f754b8c4262625ee"},
+ {file = "zipp-3.19.2-py3-none-any.whl", hash = "sha256:f091755f667055f2d02b32c53771a7a6c8b47e1fdbc4b72a8b9072b3eef8015c"},
+ {file = "zipp-3.19.2.tar.gz", hash = "sha256:bf1dcf6450f873a13e952a29504887c89e6de7506209e5b1bcc3460135d4de19"},
]
[package.extras]
-docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"]
-testing = ["big-O", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more-itertools", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-ignore-flaky", "pytest-mypy", "pytest-ruff (>=0.2.1)"]
+doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"]
+test = ["big-O", "importlib-resources", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more-itertools", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-ignore-flaky", "pytest-mypy", "pytest-ruff (>=0.2.1)"]
[extras]
all = ["aws-xray-sdk", "fastjsonschema", "pydantic"]
@@ -3669,4 +3716,4 @@ validation = ["fastjsonschema"]
[metadata]
lock-version = "2.0"
python-versions = ">=3.8,<4.0.0"
-content-hash = "039a114f8790fd1e9b52dff6dea1862765ac0a60f19ecc4aa51593f00f7e338f"
+content-hash = "cc1c9b1ef8383fa079573e85ade4d4f11fac34921d2f6c48f8392baecf7f095d"
diff --git a/provenance/2.39.2a0/multiple.intoto.jsonl b/provenance/2.39.2a0/multiple.intoto.jsonl
new file mode 100644
index 00000000000..9df3e23929b
--- /dev/null
+++ b/provenance/2.39.2a0/multiple.intoto.jsonl
@@ -0,0 +1 @@
+{"payloadType":"application/vnd.in-toto+json","payload":"eyJfdHlwZSI6Imh0dHBzOi8vaW4tdG90by5pby9TdGF0ZW1lbnQvdjAuMSIsInByZWRpY2F0ZVR5cGUiOiJodHRwczovL3Nsc2EuZGV2L3Byb3ZlbmFuY2UvdjAuMiIsInN1YmplY3QiOlt7Im5hbWUiOiIuL2F3c19sYW1iZGFfcG93ZXJ0b29scy0yLjM5LjJhMC1weTMtbm9uZS1hbnkud2hsIiwiZGlnZXN0Ijp7InNoYTI1NiI6IjlhZGY0MDc0N2ExMzRkNGE2MTFmOWY5YThiN2NjZjdkMDI2ZTUxYzliMTc5MjFiYWI4NDc1MzZlYzQ4NzQzNDkifX0seyJuYW1lIjoiLi9hd3NfbGFtYmRhX3Bvd2VydG9vbHMtMi4zOS4yYTAudGFyLmd6IiwiZGlnZXN0Ijp7InNoYTI1NiI6ImNmOWIyNzU5ZDE3ODNlOTVjODQwOTc0ZWIwYmNlNzBjYjBmZWEyMWE1ZDI2OTA1ODkwOTViNjQyMTFlZTA4MWUifX1dLCJwcmVkaWNhdGUiOnsiYnVpbGRlciI6eyJpZCI6Imh0dHBzOi8vZ2l0aHViLmNvbS9zbHNhLWZyYW1ld29yay9zbHNhLWdpdGh1Yi1nZW5lcmF0b3IvLmdpdGh1Yi93b3JrZmxvd3MvZ2VuZXJhdG9yX2dlbmVyaWNfc2xzYTMueW1sQHJlZnMvdGFncy92Mi4wLjAifSwiYnVpbGRUeXBlIjoiaHR0cHM6Ly9naXRodWIuY29tL3Nsc2EtZnJhbWV3b3JrL3Nsc2EtZ2l0aHViLWdlbmVyYXRvci9nZW5lcmljQHYxIiwiaW52b2NhdGlvbiI6eyJjb25maWdTb3VyY2UiOnsidXJpIjoiZ2l0K2h0dHBzOi8vZ2l0aHViLmNvbS9hd3MtcG93ZXJ0b29scy9wb3dlcnRvb2xzLWxhbWJkYS1weXRob25AcmVmcy9oZWFkcy9kZXZlbG9wIiwiZGlnZXN0Ijp7InNoYTEiOiI2NzgyYjZjMWEwYzVkYjdlNzgzMzU2OWY0MTFiZjNjMzMxZTg3M2JmIn0sImVudHJ5UG9pbnQiOiIuZ2l0aHViL3dvcmtmbG93cy9wcmUtcmVsZWFzZS55bWwifSwicGFyYW1ldGVycyI6eyJldmVudF9pbnB1dHMiOnsic2tpcF9jb2RlX3F1YWxpdHkiOiJmYWxzZSIsInNraXBfcHlwaSI6ImZhbHNlIn19LCJlbnZpcm9ubWVudCI6eyJnaXRodWJfYWN0b3IiOiJsZWFuZHJvZGFtYXNjZW5hIiwiZ2l0aHViX2FjdG9yX2lkIjoiNDI5NTE3MyIsImdpdGh1Yl9iYXNlX3JlZiI6IiIsImdpdGh1Yl9ldmVudF9uYW1lIjoid29ya2Zsb3dfZGlzcGF0Y2giLCJnaXRodWJfZXZlbnRfcGF5bG9hZCI6eyJlbnRlcnByaXNlIjp7ImF2YXRhcl91cmwiOiJodHRwczovL2F2YXRhcnMuZ2l0aHVidXNlcmNvbnRlbnQuY29tL2IvMTI5MD92PTQiLCJjcmVhdGVkX2F0IjoiMjAxOS0xMS0xM1QxODowNTo0MVoiLCJkZXNjcmlwdGlvbiI6IiIsImh0bWxfdXJsIjoiaHR0cHM6Ly9naXRodWIuY29tL2VudGVycHJpc2VzL2FtYXpvbiIsImlkIjoxMjkwLCJuYW1lIjoiQW1hem9uIiwibm9kZV9pZCI6Ik1ERXdPa1Z1ZEdWeWNISnBjMlV4TWprdyIsInNsdWciOiJhbWF6b24iLCJ1cGRhdGVkX2F0IjoiMjAyMy0wMS0yN1QxNDo1NjoxMFoiLCJ3ZWJzaXRlX3VybCI6Imh0dHBzOi8vd3d3LmFtYXpvbi5jb20vIn0sImlucHV0cyI6eyJza2lwX2NvZGVfcXVhbGl0eSI6ImZhbHNlIiwic2tpcF9weXBpIjoiZmFsc2UifSwib3JnYW5pemF0aW9uIjp7ImF2YXRhcl91cmwiOiJodHRwczovL2F2YXRhcnMuZ2l0aHVidXNlcmNvbnRlbnQuY29tL3UvMTI5MTI3NjM4P3Y9NCIsImRlc2NyaXB0aW9uIjoiIiwiZXZlbnRzX3VybCI6Imh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vb3Jncy9hd3MtcG93ZXJ0b29scy9ldmVudHMiLCJob29rc191cmwiOiJodHRwczovL2FwaS5naXRodWIuY29tL29yZ3MvYXdzLXBvd2VydG9vbHMvaG9va3MiLCJpZCI6MTI5MTI3NjM4LCJpc3N1ZXNfdXJsIjoiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9vcmdzL2F3cy1wb3dlcnRvb2xzL2lzc3VlcyIsImxvZ2luIjoiYXdzLXBvd2VydG9vbHMiLCJtZW1iZXJzX3VybCI6Imh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vb3Jncy9hd3MtcG93ZXJ0b29scy9tZW1iZXJzey9tZW1iZXJ9Iiwibm9kZV9pZCI6Ik9fa2dET0I3SlUxZyIsInB1YmxpY19tZW1iZXJzX3VybCI6Imh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vb3Jncy9hd3MtcG93ZXJ0b29scy9wdWJsaWNfbWVtYmVyc3svbWVtYmVyfSIsInJlcG9zX3VybCI6Imh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vb3Jncy9hd3MtcG93ZXJ0b29scy9yZXBvcyIsInVybCI6Imh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vb3Jncy9hd3MtcG93ZXJ0b29scyJ9LCJyZWYiOiJyZWZzL2hlYWRzL2RldmVsb3AiLCJyZXBvc2l0b3J5Ijp7ImFsbG93X2ZvcmtpbmciOnRydWUsImFyY2hpdmVfdXJsIjoiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9hd3MtcG93ZXJ0b29scy9wb3dlcnRvb2xzLWxhbWJkYS1weXRob24ve2FyY2hpdmVfZm9ybWF0fXsvcmVmfSIsImFyY2hpdmVkIjpmYWxzZSwiYXNzaWduZWVzX3VybCI6Imh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYXdzLXBvd2VydG9vbHMvcG93ZXJ0b29scy1sYW1iZGEtcHl0aG9uL2Fzc2lnbmVlc3svdXNlcn0iLCJibG9ic191cmwiOiJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2F3cy1wb3dlcnRvb2xzL3Bvd2VydG9vbHMtbGFtYmRhLXB5dGhvbi9naXQvYmxvYnN7L3NoYX0iLCJicmFuY2hlc191cmwiOiJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2F3cy1wb3dlcnRvb2xzL3Bvd2VydG9vbHMtbGFtYmRhLXB5dGhvbi9icmFuY2hlc3svYnJhbmNofSIsImNsb25lX3VybCI6Imh0dHBzOi8vZ2l0aHViLmNvbS9hd3MtcG93ZXJ0b29scy9wb3dlcnRvb2xzLWxhbWJkYS1weXRob24uZ2l0IiwiY29sbGFib3JhdG9yc191cmwiOiJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2F3cy1wb3dlcnRvb2xzL3Bvd2VydG9vbHMtbGFtYmRhLXB5dGhvbi9jb2xsYWJvcmF0b3Jzey9jb2xsYWJvcmF0b3J9IiwiY29tbWVudHNfdXJsIjoiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9hd3MtcG93ZXJ0b29scy9wb3dlcnRvb2xzLWxhbWJkYS1weXRob24vY29tbWVudHN7L251bWJlcn0iLCJjb21taXRzX3VybCI6Imh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYXdzLXBvd2VydG9vbHMvcG93ZXJ0b29scy1sYW1iZGEtcHl0aG9uL2NvbW1pdHN7L3NoYX0iLCJjb21wYXJlX3VybCI6Imh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYXdzLXBvd2VydG9vbHMvcG93ZXJ0b29scy1sYW1iZGEtcHl0aG9uL2NvbXBhcmUve2Jhc2V9Li4ue2hlYWR9IiwiY29udGVudHNfdXJsIjoiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9hd3MtcG93ZXJ0b29scy9wb3dlcnRvb2xzLWxhbWJkYS1weXRob24vY29udGVudHMveytwYXRofSIsImNvbnRyaWJ1dG9yc191cmwiOiJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2F3cy1wb3dlcnRvb2xzL3Bvd2VydG9vbHMtbGFtYmRhLXB5dGhvbi9jb250cmlidXRvcnMiLCJjcmVhdGVkX2F0IjoiMjAxOS0xMS0xNVQxMjoyNjoxMloiLCJjdXN0b21fcHJvcGVydGllcyI6e30sImRlZmF1bHRfYnJhbmNoIjoiZGV2ZWxvcCIsImRlcGxveW1lbnRzX3VybCI6Imh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYXdzLXBvd2VydG9vbHMvcG93ZXJ0b29scy1sYW1iZGEtcHl0aG9uL2RlcGxveW1lbnRzIiwiZGVzY3JpcHRpb24iOiJBIGRldmVsb3BlciB0b29sa2l0IHRvIGltcGxlbWVudCBTZXJ2ZXJsZXNzIGJlc3QgcHJhY3RpY2VzIGFuZCBpbmNyZWFzZSBkZXZlbG9wZXIgdmVsb2NpdHkuIiwiZGlzYWJsZWQiOmZhbHNlLCJkb3dubG9hZHNfdXJsIjoiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9hd3MtcG93ZXJ0b29scy9wb3dlcnRvb2xzLWxhbWJkYS1weXRob24vZG93bmxvYWRzIiwiZXZlbnRzX3VybCI6Imh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYXdzLXBvd2VydG9vbHMvcG93ZXJ0b29scy1sYW1iZGEtcHl0aG9uL2V2ZW50cyIsImZvcmsiOmZhbHNlLCJmb3JrcyI6MzczLCJmb3Jrc19jb3VudCI6MzczLCJmb3Jrc191cmwiOiJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2F3cy1wb3dlcnRvb2xzL3Bvd2VydG9vbHMtbGFtYmRhLXB5dGhvbi9mb3JrcyIsImZ1bGxfbmFtZSI6ImF3cy1wb3dlcnRvb2xzL3Bvd2VydG9vbHMtbGFtYmRhLXB5dGhvbiIsImdpdF9jb21taXRzX3VybCI6Imh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYXdzLXBvd2VydG9vbHMvcG93ZXJ0b29scy1sYW1iZGEtcHl0aG9uL2dpdC9jb21taXRzey9zaGF9IiwiZ2l0X3JlZnNfdXJsIjoiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9hd3MtcG93ZXJ0b29scy9wb3dlcnRvb2xzLWxhbWJkYS1weXRob24vZ2l0L3JlZnN7L3NoYX0iLCJnaXRfdGFnc191cmwiOiJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2F3cy1wb3dlcnRvb2xzL3Bvd2VydG9vbHMtbGFtYmRhLXB5dGhvbi9naXQvdGFnc3svc2hhfSIsImdpdF91cmwiOiJnaXQ6Ly9naXRodWIuY29tL2F3cy1wb3dlcnRvb2xzL3Bvd2VydG9vbHMtbGFtYmRhLXB5dGhvbi5naXQiLCJoYXNfZGlzY3Vzc2lvbnMiOnRydWUsImhhc19kb3dubG9hZHMiOnRydWUsImhhc19pc3N1ZXMiOnRydWUsImhhc19wYWdlcyI6ZmFsc2UsImhhc19wcm9qZWN0cyI6dHJ1ZSwiaGFzX3dpa2kiOmZhbHNlLCJob21lcGFnZSI6Imh0dHBzOi8vZG9jcy5wb3dlcnRvb2xzLmF3cy5kZXYvbGFtYmRhL3B5dGhvbi9sYXRlc3QvIiwiaG9va3NfdXJsIjoiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9hd3MtcG93ZXJ0b29scy9wb3dlcnRvb2xzLWxhbWJkYS1weXRob24vaG9va3MiLCJodG1sX3VybCI6Imh0dHBzOi8vZ2l0aHViLmNvbS9hd3MtcG93ZXJ0b29scy9wb3dlcnRvb2xzLWxhbWJkYS1weXRob24iLCJpZCI6MjIxOTE5Mzc5LCJpc190ZW1wbGF0ZSI6ZmFsc2UsImlzc3VlX2NvbW1lbnRfdXJsIjoiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9hd3MtcG93ZXJ0b29scy9wb3dlcnRvb2xzLWxhbWJkYS1weXRob24vaXNzdWVzL2NvbW1lbnRzey9udW1iZXJ9IiwiaXNzdWVfZXZlbnRzX3VybCI6Imh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYXdzLXBvd2VydG9vbHMvcG93ZXJ0b29scy1sYW1iZGEtcHl0aG9uL2lzc3Vlcy9ldmVudHN7L251bWJlcn0iLCJpc3N1ZXNfdXJsIjoiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9hd3MtcG93ZXJ0b29scy9wb3dlcnRvb2xzLWxhbWJkYS1weXRob24vaXNzdWVzey9udW1iZXJ9Iiwia2V5c191cmwiOiJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2F3cy1wb3dlcnRvb2xzL3Bvd2VydG9vbHMtbGFtYmRhLXB5dGhvbi9rZXlzey9rZXlfaWR9IiwibGFiZWxzX3VybCI6Imh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYXdzLXBvd2VydG9vbHMvcG93ZXJ0b29scy1sYW1iZGEtcHl0aG9uL2xhYmVsc3svbmFtZX0iLCJsYW5ndWFnZSI6IlB5dGhvbiIsImxhbmd1YWdlc191cmwiOiJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2F3cy1wb3dlcnRvb2xzL3Bvd2VydG9vbHMtbGFtYmRhLXB5dGhvbi9sYW5ndWFnZXMiLCJsaWNlbnNlIjp7ImtleSI6Im1pdC0wIiwibmFtZSI6Ik1JVCBObyBBdHRyaWJ1dGlvbiIsIm5vZGVfaWQiOiJNRGM2VEdsalpXNXpaVFF4Iiwic3BkeF9pZCI6Ik1JVC0wIiwidXJsIjoiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9saWNlbnNlcy9taXQtMCJ9LCJtZXJnZXNfdXJsIjoiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9hd3MtcG93ZXJ0b29scy9wb3dlcnRvb2xzLWxhbWJkYS1weXRob24vbWVyZ2VzIiwibWlsZXN0b25lc191cmwiOiJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2F3cy1wb3dlcnRvb2xzL3Bvd2VydG9vbHMtbGFtYmRhLXB5dGhvbi9taWxlc3RvbmVzey9udW1iZXJ9IiwibWlycm9yX3VybCI6bnVsbCwibmFtZSI6InBvd2VydG9vbHMtbGFtYmRhLXB5dGhvbiIsIm5vZGVfaWQiOiJNREV3T2xKbGNHOXphWFJ2Y25reU1qRTVNVGt6TnprPSIsIm5vdGlmaWNhdGlvbnNfdXJsIjoiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9hd3MtcG93ZXJ0b29scy9wb3dlcnRvb2xzLWxhbWJkYS1weXRob24vbm90aWZpY2F0aW9uc3s/c2luY2UsYWxsLHBhcnRpY2lwYXRpbmd9Iiwib3Blbl9pc3N1ZXMiOjkzLCJvcGVuX2lzc3Vlc19jb3VudCI6OTMsIm93bmVyIjp7ImF2YXRhcl91cmwiOiJodHRwczovL2F2YXRhcnMuZ2l0aHVidXNlcmNvbnRlbnQuY29tL3UvMTI5MTI3NjM4P3Y9NCIsImV2ZW50c191cmwiOiJodHRwczovL2FwaS5naXRodWIuY29tL3VzZXJzL2F3cy1wb3dlcnRvb2xzL2V2ZW50c3svcHJpdmFjeX0iLCJmb2xsb3dlcnNfdXJsIjoiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS91c2Vycy9hd3MtcG93ZXJ0b29scy9mb2xsb3dlcnMiLCJmb2xsb3dpbmdfdXJsIjoiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS91c2Vycy9hd3MtcG93ZXJ0b29scy9mb2xsb3dpbmd7L290aGVyX3VzZXJ9IiwiZ2lzdHNfdXJsIjoiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS91c2Vycy9hd3MtcG93ZXJ0b29scy9naXN0c3svZ2lzdF9pZH0iLCJncmF2YXRhcl9pZCI6IiIsImh0bWxfdXJsIjoiaHR0cHM6Ly9naXRodWIuY29tL2F3cy1wb3dlcnRvb2xzIiwiaWQiOjEyOTEyNzYzOCwibG9naW4iOiJhd3MtcG93ZXJ0b29scyIsIm5vZGVfaWQiOiJPX2tnRE9CN0pVMWciLCJvcmdhbml6YXRpb25zX3VybCI6Imh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vdXNlcnMvYXdzLXBvd2VydG9vbHMvb3JncyIsInJlY2VpdmVkX2V2ZW50c191cmwiOiJodHRwczovL2FwaS5naXRodWIuY29tL3VzZXJzL2F3cy1wb3dlcnRvb2xzL3JlY2VpdmVkX2V2ZW50cyIsInJlcG9zX3VybCI6Imh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vdXNlcnMvYXdzLXBvd2VydG9vbHMvcmVwb3MiLCJzaXRlX2FkbWluIjpmYWxzZSwic3RhcnJlZF91cmwiOiJodHRwczovL2FwaS5naXRodWIuY29tL3VzZXJzL2F3cy1wb3dlcnRvb2xzL3N0YXJyZWR7L293bmVyfXsvcmVwb30iLCJzdWJzY3JpcHRpb25zX3VybCI6Imh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vdXNlcnMvYXdzLXBvd2VydG9vbHMvc3Vic2NyaXB0aW9ucyIsInR5cGUiOiJPcmdhbml6YXRpb24iLCJ1cmwiOiJodHRwczovL2FwaS5naXRodWIuY29tL3VzZXJzL2F3cy1wb3dlcnRvb2xzIn0sInByaXZhdGUiOmZhbHNlLCJwdWxsc191cmwiOiJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2F3cy1wb3dlcnRvb2xzL3Bvd2VydG9vbHMtbGFtYmRhLXB5dGhvbi9wdWxsc3svbnVtYmVyfSIsInB1c2hlZF9hdCI6IjIwMjQtMDYtMjBUMTU6MzQ6NDlaIiwicmVsZWFzZXNfdXJsIjoiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9hd3MtcG93ZXJ0b29scy9wb3dlcnRvb2xzLWxhbWJkYS1weXRob24vcmVsZWFzZXN7L2lkfSIsInNpemUiOjQ1ODYzLCJzc2hfdXJsIjoiZ2l0QGdpdGh1Yi5jb206YXdzLXBvd2VydG9vbHMvcG93ZXJ0b29scy1sYW1iZGEtcHl0aG9uLmdpdCIsInN0YXJnYXplcnNfY291bnQiOjI3MDUsInN0YXJnYXplcnNfdXJsIjoiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9hd3MtcG93ZXJ0b29scy9wb3dlcnRvb2xzLWxhbWJkYS1weXRob24vc3RhcmdhemVycyIsInN0YXR1c2VzX3VybCI6Imh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYXdzLXBvd2VydG9vbHMvcG93ZXJ0b29scy1sYW1iZGEtcHl0aG9uL3N0YXR1c2VzL3tzaGF9Iiwic3Vic2NyaWJlcnNfdXJsIjoiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9hd3MtcG93ZXJ0b29scy9wb3dlcnRvb2xzLWxhbWJkYS1weXRob24vc3Vic2NyaWJlcnMiLCJzdWJzY3JpcHRpb25fdXJsIjoiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9hd3MtcG93ZXJ0b29scy9wb3dlcnRvb2xzLWxhbWJkYS1weXRob24vc3Vic2NyaXB0aW9uIiwic3ZuX3VybCI6Imh0dHBzOi8vZ2l0aHViLmNvbS9hd3MtcG93ZXJ0b29scy9wb3dlcnRvb2xzLWxhbWJkYS1weXRob24iLCJ0YWdzX3VybCI6Imh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYXdzLXBvd2VydG9vbHMvcG93ZXJ0b29scy1sYW1iZGEtcHl0aG9uL3RhZ3MiLCJ0ZWFtc191cmwiOiJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2F3cy1wb3dlcnRvb2xzL3Bvd2VydG9vbHMtbGFtYmRhLXB5dGhvbi90ZWFtcyIsInRvcGljcyI6WyJhd3MiLCJhd3MtbGFtYmRhIiwiaGFja3RvYmVyZmVzdCIsImxhbWJkYSIsInB5dGhvbiIsInNlcnZlcmxlc3MiXSwidHJlZXNfdXJsIjoiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9hd3MtcG93ZXJ0b29scy9wb3dlcnRvb2xzLWxhbWJkYS1weXRob24vZ2l0L3RyZWVzey9zaGF9IiwidXBkYXRlZF9hdCI6IjIwMjQtMDYtMjBUMTU6MzM6NDJaIiwidXJsIjoiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9hd3MtcG93ZXJ0b29scy9wb3dlcnRvb2xzLWxhbWJkYS1weXRob24iLCJ2aXNpYmlsaXR5IjoicHVibGljIiwid2F0Y2hlcnMiOjI3MDUsIndhdGNoZXJzX2NvdW50IjoyNzA1LCJ3ZWJfY29tbWl0X3NpZ25vZmZfcmVxdWlyZWQiOnRydWV9LCJzZW5kZXIiOnsiYXZhdGFyX3VybCI6Imh0dHBzOi8vYXZhdGFycy5naXRodWJ1c2VyY29udGVudC5jb20vdS80Mjk1MTczP3Y9NCIsImV2ZW50c191cmwiOiJodHRwczovL2FwaS5naXRodWIuY29tL3VzZXJzL2xlYW5kcm9kYW1hc2NlbmEvZXZlbnRzey9wcml2YWN5fSIsImZvbGxvd2Vyc191cmwiOiJodHRwczovL2FwaS5naXRodWIuY29tL3VzZXJzL2xlYW5kcm9kYW1hc2NlbmEvZm9sbG93ZXJzIiwiZm9sbG93aW5nX3VybCI6Imh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vdXNlcnMvbGVhbmRyb2RhbWFzY2VuYS9mb2xsb3dpbmd7L290aGVyX3VzZXJ9IiwiZ2lzdHNfdXJsIjoiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS91c2Vycy9sZWFuZHJvZGFtYXNjZW5hL2dpc3Rzey9naXN0X2lkfSIsImdyYXZhdGFyX2lkIjoiIiwiaHRtbF91cmwiOiJodHRwczovL2dpdGh1Yi5jb20vbGVhbmRyb2RhbWFzY2VuYSIsImlkIjo0Mjk1MTczLCJsb2dpbiI6ImxlYW5kcm9kYW1hc2NlbmEiLCJub2RlX2lkIjoiTURRNlZYTmxjalF5T1RVeE56TT0iLCJvcmdhbml6YXRpb25zX3VybCI6Imh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vdXNlcnMvbGVhbmRyb2RhbWFzY2VuYS9vcmdzIiwicmVjZWl2ZWRfZXZlbnRzX3VybCI6Imh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vdXNlcnMvbGVhbmRyb2RhbWFzY2VuYS9yZWNlaXZlZF9ldmVudHMiLCJyZXBvc191cmwiOiJodHRwczovL2FwaS5naXRodWIuY29tL3VzZXJzL2xlYW5kcm9kYW1hc2NlbmEvcmVwb3MiLCJzaXRlX2FkbWluIjpmYWxzZSwic3RhcnJlZF91cmwiOiJodHRwczovL2FwaS5naXRodWIuY29tL3VzZXJzL2xlYW5kcm9kYW1hc2NlbmEvc3RhcnJlZHsvb3duZXJ9ey9yZXBvfSIsInN1YnNjcmlwdGlvbnNfdXJsIjoiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS91c2Vycy9sZWFuZHJvZGFtYXNjZW5hL3N1YnNjcmlwdGlvbnMiLCJ0eXBlIjoiVXNlciIsInVybCI6Imh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vdXNlcnMvbGVhbmRyb2RhbWFzY2VuYSJ9LCJ3b3JrZmxvdyI6Ii5naXRodWIvd29ya2Zsb3dzL3ByZS1yZWxlYXNlLnltbCJ9LCJnaXRodWJfaGVhZF9yZWYiOiIiLCJnaXRodWJfcmVmIjoicmVmcy9oZWFkcy9kZXZlbG9wIiwiZ2l0aHViX3JlZl90eXBlIjoiYnJhbmNoIiwiZ2l0aHViX3JlcG9zaXRvcnlfaWQiOiIyMjE5MTkzNzkiLCJnaXRodWJfcmVwb3NpdG9yeV9vd25lciI6ImF3cy1wb3dlcnRvb2xzIiwiZ2l0aHViX3JlcG9zaXRvcnlfb3duZXJfaWQiOiIxMjkxMjc2MzgiLCJnaXRodWJfcnVuX2F0dGVtcHQiOiIxIiwiZ2l0aHViX3J1bl9pZCI6Ijk2MDAzNjU2OTUiLCJnaXRodWJfcnVuX251bWJlciI6IjEiLCJnaXRodWJfc2hhMSI6IjY3ODJiNmMxYTBjNWRiN2U3ODMzNTY5ZjQxMWJmM2MzMzFlODczYmYifX0sIm1ldGFkYXRhIjp7ImJ1aWxkSW52b2NhdGlvbklEIjoiOTYwMDM2NTY5NS0xIiwiY29tcGxldGVuZXNzIjp7InBhcmFtZXRlcnMiOnRydWUsImVudmlyb25tZW50IjpmYWxzZSwibWF0ZXJpYWxzIjpmYWxzZX0sInJlcHJvZHVjaWJsZSI6ZmFsc2V9LCJtYXRlcmlhbHMiOlt7InVyaSI6ImdpdCtodHRwczovL2dpdGh1Yi5jb20vYXdzLXBvd2VydG9vbHMvcG93ZXJ0b29scy1sYW1iZGEtcHl0aG9uQHJlZnMvaGVhZHMvZGV2ZWxvcCIsImRpZ2VzdCI6eyJzaGExIjoiNjc4MmI2YzFhMGM1ZGI3ZTc4MzM1NjlmNDExYmYzYzMzMWU4NzNiZiJ9fV19fQ==","signatures":[{"keyid":"","sig":"MEUCIF5jIdfPaXdXkrWip0720tiRwn4j1Eom0G2OvnxJwVkKAiEA8satyMfu+l9oE0ZhkNwojxyOaO1qytj5zSn2XdN9WbE=","cert":"-----BEGIN CERTIFICATE-----\nMIIHeDCCBv2gAwIBAgIUCgvA2fhYI7BnH3A63Eni4yC1opIwCgYIKoZIzj0EAwMw\nNzEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MR4wHAYDVQQDExVzaWdzdG9yZS1pbnRl\ncm1lZGlhdGUwHhcNMjQwNjIwMTU0MzM0WhcNMjQwNjIwMTU1MzM0WjAAMFkwEwYH\nKoZIzj0CAQYIKoZIzj0DAQcDQgAEStOmYfq7I9EylrJCbF5VSul+bEfxdfFhdU6s\nEHXBgCdkNPySyO9eKYNNQ2XacWk70dmDHWLGY5gKu0PLaq/dVKOCBhwwggYYMA4G\nA1UdDwEB/wQEAwIHgDATBgNVHSUEDDAKBggrBgEFBQcDAzAdBgNVHQ4EFgQUXX7Y\naWoK72E4hb/mju6cotj3gNQwHwYDVR0jBBgwFoAU39Ppz1YkEZb5qNjpKFWixi4Y\nZD8wgYQGA1UdEQEB/wR6MHiGdmh0dHBzOi8vZ2l0aHViLmNvbS9zbHNhLWZyYW1l\nd29yay9zbHNhLWdpdGh1Yi1nZW5lcmF0b3IvLmdpdGh1Yi93b3JrZmxvd3MvZ2Vu\nZXJhdG9yX2dlbmVyaWNfc2xzYTMueW1sQHJlZnMvdGFncy92Mi4wLjAwOQYKKwYB\nBAGDvzABAQQraHR0cHM6Ly90b2tlbi5hY3Rpb25zLmdpdGh1YnVzZXJjb250ZW50\nLmNvbTAfBgorBgEEAYO/MAECBBF3b3JrZmxvd19kaXNwYXRjaDA2BgorBgEEAYO/\nMAEDBCg2NzgyYjZjMWEwYzVkYjdlNzgzMzU2OWY0MTFiZjNjMzMxZTg3M2JmMBkG\nCisGAQQBg78wAQQEC1ByZS1SZWxlYXNlMDUGCisGAQQBg78wAQUEJ2F3cy1wb3dl\ncnRvb2xzL3Bvd2VydG9vbHMtbGFtYmRhLXB5dGhvbjAgBgorBgEEAYO/MAEGBBJy\nZWZzL2hlYWRzL2RldmVsb3AwOwYKKwYBBAGDvzABCAQtDCtodHRwczovL3Rva2Vu\nLmFjdGlvbnMuZ2l0aHVidXNlcmNvbnRlbnQuY29tMIGGBgorBgEEAYO/MAEJBHgM\ndmh0dHBzOi8vZ2l0aHViLmNvbS9zbHNhLWZyYW1ld29yay9zbHNhLWdpdGh1Yi1n\nZW5lcmF0b3IvLmdpdGh1Yi93b3JrZmxvd3MvZ2VuZXJhdG9yX2dlbmVyaWNfc2xz\nYTMueW1sQHJlZnMvdGFncy92Mi4wLjAwOAYKKwYBBAGDvzABCgQqDCg1YTc3NWIz\nNjdhNTZkNWJkMTE4YTIyNGE4MTFiYmEyODgxNTBhNTYzMB0GCisGAQQBg78wAQsE\nDwwNZ2l0aHViLWhvc3RlZDBKBgorBgEEAYO/MAEMBDwMOmh0dHBzOi8vZ2l0aHVi\nLmNvbS9hd3MtcG93ZXJ0b29scy9wb3dlcnRvb2xzLWxhbWJkYS1weXRob24wOAYK\nKwYBBAGDvzABDQQqDCg2NzgyYjZjMWEwYzVkYjdlNzgzMzU2OWY0MTFiZjNjMzMx\nZTg3M2JmMCIGCisGAQQBg78wAQ4EFAwScmVmcy9oZWFkcy9kZXZlbG9wMBkGCisG\nAQQBg78wAQ8ECwwJMjIxOTE5Mzc5MDEGCisGAQQBg78wARAEIwwhaHR0cHM6Ly9n\naXRodWIuY29tL2F3cy1wb3dlcnRvb2xzMBkGCisGAQQBg78wAREECwwJMTI5MTI3\nNjM4MH8GCisGAQQBg78wARIEcQxvaHR0cHM6Ly9naXRodWIuY29tL2F3cy1wb3dl\ncnRvb2xzL3Bvd2VydG9vbHMtbGFtYmRhLXB5dGhvbi8uZ2l0aHViL3dvcmtmbG93\ncy9wcmUtcmVsZWFzZS55bWxAcmVmcy9oZWFkcy9kZXZlbG9wMDgGCisGAQQBg78w\nARMEKgwoNjc4MmI2YzFhMGM1ZGI3ZTc4MzM1NjlmNDExYmYzYzMzMWU4NzNiZjAh\nBgorBgEEAYO/MAEUBBMMEXdvcmtmbG93X2Rpc3BhdGNoMG0GCisGAQQBg78wARUE\nXwxdaHR0cHM6Ly9naXRodWIuY29tL2F3cy1wb3dlcnRvb2xzL3Bvd2VydG9vbHMt\nbGFtYmRhLXB5dGhvbi9hY3Rpb25zL3J1bnMvOTYwMDM2NTY5NS9hdHRlbXB0cy8x\nMBYGCisGAQQBg78wARYECAwGcHVibGljMIGKBgorBgEEAdZ5AgQCBHwEegB4AHYA\n3T0wasbHETJjGR4cmWc3AqJKXrjePK3/h4pygC8p7o4AAAGQNlFEqAAABAMARzBF\nAiAuUkSfr4WR2BeNTG/NNHmVXaplabzlQ80gQv/Vd1VptwIhAPHOvqivE+h9GFxV\n6t7wBm2VwMUfGCRqilV3LxDiVNojMAoGCCqGSM49BAMDA2kAMGYCMQC9PEtLQ87i\n15wx85VP2QdKPTwHa8guL4c/zc42wS5LgpAT+nlHTtsjjkD4r+sBgg0CMQCqbnVE\njTK1lCTqcL37pZNu0gcDlIfTZ6+FMdTK1Gtks+H93WksCyruINl/Q7SghTw=\n-----END CERTIFICATE-----\n"}]}
\ No newline at end of file
diff --git a/provenance/2.39.2a1/multiple.intoto.jsonl b/provenance/2.39.2a1/multiple.intoto.jsonl
new file mode 100644
index 00000000000..5e949878203
--- /dev/null
+++ b/provenance/2.39.2a1/multiple.intoto.jsonl
@@ -0,0 +1 @@
+{"payloadType":"application/vnd.in-toto+json","payload":"eyJfdHlwZSI6Imh0dHBzOi8vaW4tdG90by5pby9TdGF0ZW1lbnQvdjAuMSIsInByZWRpY2F0ZVR5cGUiOiJodHRwczovL3Nsc2EuZGV2L3Byb3ZlbmFuY2UvdjAuMiIsInN1YmplY3QiOlt7Im5hbWUiOiIuL2F3c19sYW1iZGFfcG93ZXJ0b29scy0yLjM5LjJhMS1weTMtbm9uZS1hbnkud2hsIiwiZGlnZXN0Ijp7InNoYTI1NiI6ImNiNTg1NTA5MGQwODc5OGJlOTJhYWE4ZGM5MzdmZjY0MWM2OTc1MmZmYWY1YjYzYTJkMGNlYzc0MmM3MWZhYmIifX0seyJuYW1lIjoiLi9hd3NfbGFtYmRhX3Bvd2VydG9vbHMtMi4zOS4yYTEudGFyLmd6IiwiZGlnZXN0Ijp7InNoYTI1NiI6ImJhN2Y5MDlhYWI2MTEyZWQxZWZlMWQ5N2FlMDQ4NmVhOTA1NDgxZTNlYWEyMTRhOTNjNDUyNGQ1OTk3YWI2YmQifX1dLCJwcmVkaWNhdGUiOnsiYnVpbGRlciI6eyJpZCI6Imh0dHBzOi8vZ2l0aHViLmNvbS9zbHNhLWZyYW1ld29yay9zbHNhLWdpdGh1Yi1nZW5lcmF0b3IvLmdpdGh1Yi93b3JrZmxvd3MvZ2VuZXJhdG9yX2dlbmVyaWNfc2xzYTMueW1sQHJlZnMvdGFncy92Mi4wLjAifSwiYnVpbGRUeXBlIjoiaHR0cHM6Ly9naXRodWIuY29tL3Nsc2EtZnJhbWV3b3JrL3Nsc2EtZ2l0aHViLWdlbmVyYXRvci9nZW5lcmljQHYxIiwiaW52b2NhdGlvbiI6eyJjb25maWdTb3VyY2UiOnsidXJpIjoiZ2l0K2h0dHBzOi8vZ2l0aHViLmNvbS9hd3MtcG93ZXJ0b29scy9wb3dlcnRvb2xzLWxhbWJkYS1weXRob25AcmVmcy9oZWFkcy9kZXZlbG9wIiwiZGlnZXN0Ijp7InNoYTEiOiJkZTliZmNjNGI5NzhhYmE3YjBiYmM3Y2FjMGZlM2RkZmYyZWFlYzc1In0sImVudHJ5UG9pbnQiOiIuZ2l0aHViL3dvcmtmbG93cy9wcmUtcmVsZWFzZS55bWwifSwicGFyYW1ldGVycyI6e30sImVudmlyb25tZW50Ijp7ImdpdGh1Yl9hY3RvciI6ImxlYW5kcm9kYW1hc2NlbmEiLCJnaXRodWJfYWN0b3JfaWQiOiI0Mjk1MTczIiwiZ2l0aHViX2Jhc2VfcmVmIjoiIiwiZ2l0aHViX2V2ZW50X25hbWUiOiJzY2hlZHVsZSIsImdpdGh1Yl9ldmVudF9wYXlsb2FkIjp7ImVudGVycHJpc2UiOnsiYXZhdGFyX3VybCI6Imh0dHBzOi8vYXZhdGFycy5naXRodWJ1c2VyY29udGVudC5jb20vYi8xMjkwP3Y9NCIsImNyZWF0ZWRfYXQiOiIyMDE5LTExLTEzVDE4OjA1OjQxWiIsImRlc2NyaXB0aW9uIjoiIiwiaHRtbF91cmwiOiJodHRwczovL2dpdGh1Yi5jb20vZW50ZXJwcmlzZXMvYW1hem9uIiwiaWQiOjEyOTAsIm5hbWUiOiJBbWF6b24iLCJub2RlX2lkIjoiTURFd09rVnVkR1Z5Y0hKcGMyVXhNamt3Iiwic2x1ZyI6ImFtYXpvbiIsInVwZGF0ZWRfYXQiOiIyMDIzLTAxLTI3VDE0OjU2OjEwWiIsIndlYnNpdGVfdXJsIjoiaHR0cHM6Ly93d3cuYW1hem9uLmNvbS8ifSwib3JnYW5pemF0aW9uIjp7ImF2YXRhcl91cmwiOiJodHRwczovL2F2YXRhcnMuZ2l0aHVidXNlcmNvbnRlbnQuY29tL3UvMTI5MTI3NjM4P3Y9NCIsImRlc2NyaXB0aW9uIjoiIiwiZXZlbnRzX3VybCI6Imh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vb3Jncy9hd3MtcG93ZXJ0b29scy9ldmVudHMiLCJob29rc191cmwiOiJodHRwczovL2FwaS5naXRodWIuY29tL29yZ3MvYXdzLXBvd2VydG9vbHMvaG9va3MiLCJpZCI6MTI5MTI3NjM4LCJpc3N1ZXNfdXJsIjoiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9vcmdzL2F3cy1wb3dlcnRvb2xzL2lzc3VlcyIsImxvZ2luIjoiYXdzLXBvd2VydG9vbHMiLCJtZW1iZXJzX3VybCI6Imh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vb3Jncy9hd3MtcG93ZXJ0b29scy9tZW1iZXJzey9tZW1iZXJ9Iiwibm9kZV9pZCI6Ik9fa2dET0I3SlUxZyIsInB1YmxpY19tZW1iZXJzX3VybCI6Imh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vb3Jncy9hd3MtcG93ZXJ0b29scy9wdWJsaWNfbWVtYmVyc3svbWVtYmVyfSIsInJlcG9zX3VybCI6Imh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vb3Jncy9hd3MtcG93ZXJ0b29scy9yZXBvcyIsInVybCI6Imh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vb3Jncy9hd3MtcG93ZXJ0b29scyJ9LCJyZXBvc2l0b3J5Ijp7ImFsbG93X2ZvcmtpbmciOnRydWUsImFyY2hpdmVfdXJsIjoiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9hd3MtcG93ZXJ0b29scy9wb3dlcnRvb2xzLWxhbWJkYS1weXRob24ve2FyY2hpdmVfZm9ybWF0fXsvcmVmfSIsImFyY2hpdmVkIjpmYWxzZSwiYXNzaWduZWVzX3VybCI6Imh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYXdzLXBvd2VydG9vbHMvcG93ZXJ0b29scy1sYW1iZGEtcHl0aG9uL2Fzc2lnbmVlc3svdXNlcn0iLCJibG9ic191cmwiOiJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2F3cy1wb3dlcnRvb2xzL3Bvd2VydG9vbHMtbGFtYmRhLXB5dGhvbi9naXQvYmxvYnN7L3NoYX0iLCJicmFuY2hlc191cmwiOiJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2F3cy1wb3dlcnRvb2xzL3Bvd2VydG9vbHMtbGFtYmRhLXB5dGhvbi9icmFuY2hlc3svYnJhbmNofSIsImNsb25lX3VybCI6Imh0dHBzOi8vZ2l0aHViLmNvbS9hd3MtcG93ZXJ0b29scy9wb3dlcnRvb2xzLWxhbWJkYS1weXRob24uZ2l0IiwiY29sbGFib3JhdG9yc191cmwiOiJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2F3cy1wb3dlcnRvb2xzL3Bvd2VydG9vbHMtbGFtYmRhLXB5dGhvbi9jb2xsYWJvcmF0b3Jzey9jb2xsYWJvcmF0b3J9IiwiY29tbWVudHNfdXJsIjoiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9hd3MtcG93ZXJ0b29scy9wb3dlcnRvb2xzLWxhbWJkYS1weXRob24vY29tbWVudHN7L251bWJlcn0iLCJjb21taXRzX3VybCI6Imh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYXdzLXBvd2VydG9vbHMvcG93ZXJ0b29scy1sYW1iZGEtcHl0aG9uL2NvbW1pdHN7L3NoYX0iLCJjb21wYXJlX3VybCI6Imh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYXdzLXBvd2VydG9vbHMvcG93ZXJ0b29scy1sYW1iZGEtcHl0aG9uL2NvbXBhcmUve2Jhc2V9Li4ue2hlYWR9IiwiY29udGVudHNfdXJsIjoiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9hd3MtcG93ZXJ0b29scy9wb3dlcnRvb2xzLWxhbWJkYS1weXRob24vY29udGVudHMveytwYXRofSIsImNvbnRyaWJ1dG9yc191cmwiOiJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2F3cy1wb3dlcnRvb2xzL3Bvd2VydG9vbHMtbGFtYmRhLXB5dGhvbi9jb250cmlidXRvcnMiLCJjcmVhdGVkX2F0IjoiMjAxOS0xMS0xNVQxMjoyNjoxMloiLCJkZWZhdWx0X2JyYW5jaCI6ImRldmVsb3AiLCJkZXBsb3ltZW50c191cmwiOiJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2F3cy1wb3dlcnRvb2xzL3Bvd2VydG9vbHMtbGFtYmRhLXB5dGhvbi9kZXBsb3ltZW50cyIsImRlc2NyaXB0aW9uIjoiQSBkZXZlbG9wZXIgdG9vbGtpdCB0byBpbXBsZW1lbnQgU2VydmVybGVzcyBiZXN0IHByYWN0aWNlcyBhbmQgaW5jcmVhc2UgZGV2ZWxvcGVyIHZlbG9jaXR5LiIsImRpc2FibGVkIjpmYWxzZSwiZG93bmxvYWRzX3VybCI6Imh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYXdzLXBvd2VydG9vbHMvcG93ZXJ0b29scy1sYW1iZGEtcHl0aG9uL2Rvd25sb2FkcyIsImV2ZW50c191cmwiOiJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2F3cy1wb3dlcnRvb2xzL3Bvd2VydG9vbHMtbGFtYmRhLXB5dGhvbi9ldmVudHMiLCJmb3JrIjpmYWxzZSwiZm9ya3MiOjM3MywiZm9ya3NfY291bnQiOjM3MywiZm9ya3NfdXJsIjoiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9hd3MtcG93ZXJ0b29scy9wb3dlcnRvb2xzLWxhbWJkYS1weXRob24vZm9ya3MiLCJmdWxsX25hbWUiOiJhd3MtcG93ZXJ0b29scy9wb3dlcnRvb2xzLWxhbWJkYS1weXRob24iLCJnaXRfY29tbWl0c191cmwiOiJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2F3cy1wb3dlcnRvb2xzL3Bvd2VydG9vbHMtbGFtYmRhLXB5dGhvbi9naXQvY29tbWl0c3svc2hhfSIsImdpdF9yZWZzX3VybCI6Imh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYXdzLXBvd2VydG9vbHMvcG93ZXJ0b29scy1sYW1iZGEtcHl0aG9uL2dpdC9yZWZzey9zaGF9IiwiZ2l0X3RhZ3NfdXJsIjoiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9hd3MtcG93ZXJ0b29scy9wb3dlcnRvb2xzLWxhbWJkYS1weXRob24vZ2l0L3RhZ3N7L3NoYX0iLCJnaXRfdXJsIjoiZ2l0Oi8vZ2l0aHViLmNvbS9hd3MtcG93ZXJ0b29scy9wb3dlcnRvb2xzLWxhbWJkYS1weXRob24uZ2l0IiwiaGFzX2Rpc2N1c3Npb25zIjp0cnVlLCJoYXNfZG93bmxvYWRzIjp0cnVlLCJoYXNfaXNzdWVzIjp0cnVlLCJoYXNfcGFnZXMiOmZhbHNlLCJoYXNfcHJvamVjdHMiOnRydWUsImhhc193aWtpIjpmYWxzZSwiaG9tZXBhZ2UiOiJodHRwczovL2RvY3MucG93ZXJ0b29scy5hd3MuZGV2L2xhbWJkYS9weXRob24vbGF0ZXN0LyIsImhvb2tzX3VybCI6Imh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYXdzLXBvd2VydG9vbHMvcG93ZXJ0b29scy1sYW1iZGEtcHl0aG9uL2hvb2tzIiwiaHRtbF91cmwiOiJodHRwczovL2dpdGh1Yi5jb20vYXdzLXBvd2VydG9vbHMvcG93ZXJ0b29scy1sYW1iZGEtcHl0aG9uIiwiaWQiOjIyMTkxOTM3OSwiaXNfdGVtcGxhdGUiOmZhbHNlLCJpc3N1ZV9jb21tZW50X3VybCI6Imh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYXdzLXBvd2VydG9vbHMvcG93ZXJ0b29scy1sYW1iZGEtcHl0aG9uL2lzc3Vlcy9jb21tZW50c3svbnVtYmVyfSIsImlzc3VlX2V2ZW50c191cmwiOiJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2F3cy1wb3dlcnRvb2xzL3Bvd2VydG9vbHMtbGFtYmRhLXB5dGhvbi9pc3N1ZXMvZXZlbnRzey9udW1iZXJ9IiwiaXNzdWVzX3VybCI6Imh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYXdzLXBvd2VydG9vbHMvcG93ZXJ0b29scy1sYW1iZGEtcHl0aG9uL2lzc3Vlc3svbnVtYmVyfSIsImtleXNfdXJsIjoiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9hd3MtcG93ZXJ0b29scy9wb3dlcnRvb2xzLWxhbWJkYS1weXRob24va2V5c3sva2V5X2lkfSIsImxhYmVsc191cmwiOiJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2F3cy1wb3dlcnRvb2xzL3Bvd2VydG9vbHMtbGFtYmRhLXB5dGhvbi9sYWJlbHN7L25hbWV9IiwibGFuZ3VhZ2UiOiJQeXRob24iLCJsYW5ndWFnZXNfdXJsIjoiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9hd3MtcG93ZXJ0b29scy9wb3dlcnRvb2xzLWxhbWJkYS1weXRob24vbGFuZ3VhZ2VzIiwibGljZW5zZSI6eyJrZXkiOiJtaXQtMCIsIm5hbWUiOiJNSVQgTm8gQXR0cmlidXRpb24iLCJub2RlX2lkIjoiTURjNlRHbGpaVzV6WlRReCIsInNwZHhfaWQiOiJNSVQtMCIsInVybCI6Imh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vbGljZW5zZXMvbWl0LTAifSwibWVyZ2VzX3VybCI6Imh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYXdzLXBvd2VydG9vbHMvcG93ZXJ0b29scy1sYW1iZGEtcHl0aG9uL21lcmdlcyIsIm1pbGVzdG9uZXNfdXJsIjoiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9hd3MtcG93ZXJ0b29scy9wb3dlcnRvb2xzLWxhbWJkYS1weXRob24vbWlsZXN0b25lc3svbnVtYmVyfSIsIm1pcnJvcl91cmwiOm51bGwsIm5hbWUiOiJwb3dlcnRvb2xzLWxhbWJkYS1weXRob24iLCJub2RlX2lkIjoiTURFd09sSmxjRzl6YVhSdmNua3lNakU1TVRrek56az0iLCJub3RpZmljYXRpb25zX3VybCI6Imh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYXdzLXBvd2VydG9vbHMvcG93ZXJ0b29scy1sYW1iZGEtcHl0aG9uL25vdGlmaWNhdGlvbnN7P3NpbmNlLGFsbCxwYXJ0aWNpcGF0aW5nfSIsIm9wZW5faXNzdWVzIjo5NSwib3Blbl9pc3N1ZXNfY291bnQiOjk1LCJvd25lciI6eyJhdmF0YXJfdXJsIjoiaHR0cHM6Ly9hdmF0YXJzLmdpdGh1YnVzZXJjb250ZW50LmNvbS91LzEyOTEyNzYzOD92PTQiLCJldmVudHNfdXJsIjoiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS91c2Vycy9hd3MtcG93ZXJ0b29scy9ldmVudHN7L3ByaXZhY3l9IiwiZm9sbG93ZXJzX3VybCI6Imh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vdXNlcnMvYXdzLXBvd2VydG9vbHMvZm9sbG93ZXJzIiwiZm9sbG93aW5nX3VybCI6Imh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vdXNlcnMvYXdzLXBvd2VydG9vbHMvZm9sbG93aW5ney9vdGhlcl91c2VyfSIsImdpc3RzX3VybCI6Imh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vdXNlcnMvYXdzLXBvd2VydG9vbHMvZ2lzdHN7L2dpc3RfaWR9IiwiZ3JhdmF0YXJfaWQiOiIiLCJodG1sX3VybCI6Imh0dHBzOi8vZ2l0aHViLmNvbS9hd3MtcG93ZXJ0b29scyIsImlkIjoxMjkxMjc2MzgsImxvZ2luIjoiYXdzLXBvd2VydG9vbHMiLCJub2RlX2lkIjoiT19rZ0RPQjdKVTFnIiwib3JnYW5pemF0aW9uc191cmwiOiJodHRwczovL2FwaS5naXRodWIuY29tL3VzZXJzL2F3cy1wb3dlcnRvb2xzL29yZ3MiLCJyZWNlaXZlZF9ldmVudHNfdXJsIjoiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS91c2Vycy9hd3MtcG93ZXJ0b29scy9yZWNlaXZlZF9ldmVudHMiLCJyZXBvc191cmwiOiJodHRwczovL2FwaS5naXRodWIuY29tL3VzZXJzL2F3cy1wb3dlcnRvb2xzL3JlcG9zIiwic2l0ZV9hZG1pbiI6ZmFsc2UsInN0YXJyZWRfdXJsIjoiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS91c2Vycy9hd3MtcG93ZXJ0b29scy9zdGFycmVkey9vd25lcn17L3JlcG99Iiwic3Vic2NyaXB0aW9uc191cmwiOiJodHRwczovL2FwaS5naXRodWIuY29tL3VzZXJzL2F3cy1wb3dlcnRvb2xzL3N1YnNjcmlwdGlvbnMiLCJ0eXBlIjoiT3JnYW5pemF0aW9uIiwidXJsIjoiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS91c2Vycy9hd3MtcG93ZXJ0b29scyJ9LCJwcml2YXRlIjpmYWxzZSwicHVsbHNfdXJsIjoiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9hd3MtcG93ZXJ0b29scy9wb3dlcnRvb2xzLWxhbWJkYS1weXRob24vcHVsbHN7L251bWJlcn0iLCJwdXNoZWRfYXQiOiIyMDI0LTA2LTIwVDIyOjI1OjU4WiIsInJlbGVhc2VzX3VybCI6Imh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYXdzLXBvd2VydG9vbHMvcG93ZXJ0b29scy1sYW1iZGEtcHl0aG9uL3JlbGVhc2Vzey9pZH0iLCJzaXplIjo0NjUwMSwic3NoX3VybCI6ImdpdEBnaXRodWIuY29tOmF3cy1wb3dlcnRvb2xzL3Bvd2VydG9vbHMtbGFtYmRhLXB5dGhvbi5naXQiLCJzdGFyZ2F6ZXJzX2NvdW50IjoyNzA3LCJzdGFyZ2F6ZXJzX3VybCI6Imh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYXdzLXBvd2VydG9vbHMvcG93ZXJ0b29scy1sYW1iZGEtcHl0aG9uL3N0YXJnYXplcnMiLCJzdGF0dXNlc191cmwiOiJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2F3cy1wb3dlcnRvb2xzL3Bvd2VydG9vbHMtbGFtYmRhLXB5dGhvbi9zdGF0dXNlcy97c2hhfSIsInN1YnNjcmliZXJzX3VybCI6Imh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYXdzLXBvd2VydG9vbHMvcG93ZXJ0b29scy1sYW1iZGEtcHl0aG9uL3N1YnNjcmliZXJzIiwic3Vic2NyaXB0aW9uX3VybCI6Imh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYXdzLXBvd2VydG9vbHMvcG93ZXJ0b29scy1sYW1iZGEtcHl0aG9uL3N1YnNjcmlwdGlvbiIsInN2bl91cmwiOiJodHRwczovL2dpdGh1Yi5jb20vYXdzLXBvd2VydG9vbHMvcG93ZXJ0b29scy1sYW1iZGEtcHl0aG9uIiwidGFnc191cmwiOiJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2F3cy1wb3dlcnRvb2xzL3Bvd2VydG9vbHMtbGFtYmRhLXB5dGhvbi90YWdzIiwidGVhbXNfdXJsIjoiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9hd3MtcG93ZXJ0b29scy9wb3dlcnRvb2xzLWxhbWJkYS1weXRob24vdGVhbXMiLCJ0b3BpY3MiOlsiYXdzIiwiYXdzLWxhbWJkYSIsImhhY2t0b2JlcmZlc3QiLCJsYW1iZGEiLCJweXRob24iLCJzZXJ2ZXJsZXNzIl0sInRyZWVzX3VybCI6Imh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYXdzLXBvd2VydG9vbHMvcG93ZXJ0b29scy1sYW1iZGEtcHl0aG9uL2dpdC90cmVlc3svc2hhfSIsInVwZGF0ZWRfYXQiOiIyMDI0LTA2LTIxVDA3OjQ0OjAyWiIsInVybCI6Imh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYXdzLXBvd2VydG9vbHMvcG93ZXJ0b29scy1sYW1iZGEtcHl0aG9uIiwidmlzaWJpbGl0eSI6InB1YmxpYyIsIndhdGNoZXJzIjoyNzA3LCJ3YXRjaGVyc19jb3VudCI6MjcwNywid2ViX2NvbW1pdF9zaWdub2ZmX3JlcXVpcmVkIjp0cnVlfSwic2NoZWR1bGUiOiIwIDggKiAqIDEtNSIsIndvcmtmbG93IjoiLmdpdGh1Yi93b3JrZmxvd3MvcHJlLXJlbGVhc2UueW1sIn0sImdpdGh1Yl9oZWFkX3JlZiI6IiIsImdpdGh1Yl9yZWYiOiJyZWZzL2hlYWRzL2RldmVsb3AiLCJnaXRodWJfcmVmX3R5cGUiOiJicmFuY2giLCJnaXRodWJfcmVwb3NpdG9yeV9pZCI6IjIyMTkxOTM3OSIsImdpdGh1Yl9yZXBvc2l0b3J5X293bmVyIjoiYXdzLXBvd2VydG9vbHMiLCJnaXRodWJfcmVwb3NpdG9yeV9vd25lcl9pZCI6IjEyOTEyNzYzOCIsImdpdGh1Yl9ydW5fYXR0ZW1wdCI6IjEiLCJnaXRodWJfcnVuX2lkIjoiOTYxMDMyMDE5MyIsImdpdGh1Yl9ydW5fbnVtYmVyIjoiMiIsImdpdGh1Yl9zaGExIjoiZGU5YmZjYzRiOTc4YWJhN2IwYmJjN2NhYzBmZTNkZGZmMmVhZWM3NSJ9fSwibWV0YWRhdGEiOnsiYnVpbGRJbnZvY2F0aW9uSUQiOiI5NjEwMzIwMTkzLTEiLCJjb21wbGV0ZW5lc3MiOnsicGFyYW1ldGVycyI6dHJ1ZSwiZW52aXJvbm1lbnQiOmZhbHNlLCJtYXRlcmlhbHMiOmZhbHNlfSwicmVwcm9kdWNpYmxlIjpmYWxzZX0sIm1hdGVyaWFscyI6W3sidXJpIjoiZ2l0K2h0dHBzOi8vZ2l0aHViLmNvbS9hd3MtcG93ZXJ0b29scy9wb3dlcnRvb2xzLWxhbWJkYS1weXRob25AcmVmcy9oZWFkcy9kZXZlbG9wIiwiZGlnZXN0Ijp7InNoYTEiOiJkZTliZmNjNGI5NzhhYmE3YjBiYmM3Y2FjMGZlM2RkZmYyZWFlYzc1In19XX19","signatures":[{"keyid":"","sig":"MEUCIQCcBjysUNglIkqajbxhzI/SYnzvIrJeXO1pwvxZ+Kjm2wIgM6gWXHt0rqZ6IELqz/UbaSyCwep5do8GwfOt8k/uwFA=","cert":"-----BEGIN CERTIFICATE-----\nMIIHZTCCBuugAwIBAgIUUzeUekjPVXrxcBfKksKTcLuzP20wCgYIKoZIzj0EAwMw\nNzEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MR4wHAYDVQQDExVzaWdzdG9yZS1pbnRl\ncm1lZGlhdGUwHhcNMjQwNjIxMDgwODE5WhcNMjQwNjIxMDgxODE5WjAAMFkwEwYH\nKoZIzj0CAQYIKoZIzj0DAQcDQgAEnfNpsF0lC88vWXKdRuDxNaRp0WO2gKcZIkOj\nGdvCv06oYaLuW9h9GdZqnktqaVkR0BsgGAmASb+SYGPoGhZtj6OCBgowggYGMA4G\nA1UdDwEB/wQEAwIHgDATBgNVHSUEDDAKBggrBgEFBQcDAzAdBgNVHQ4EFgQUUcGX\nf38hj5M3zd/FgWK+5OxZGWwwHwYDVR0jBBgwFoAU39Ppz1YkEZb5qNjpKFWixi4Y\nZD8wgYQGA1UdEQEB/wR6MHiGdmh0dHBzOi8vZ2l0aHViLmNvbS9zbHNhLWZyYW1l\nd29yay9zbHNhLWdpdGh1Yi1nZW5lcmF0b3IvLmdpdGh1Yi93b3JrZmxvd3MvZ2Vu\nZXJhdG9yX2dlbmVyaWNfc2xzYTMueW1sQHJlZnMvdGFncy92Mi4wLjAwOQYKKwYB\nBAGDvzABAQQraHR0cHM6Ly90b2tlbi5hY3Rpb25zLmdpdGh1YnVzZXJjb250ZW50\nLmNvbTAWBgorBgEEAYO/MAECBAhzY2hlZHVsZTA2BgorBgEEAYO/MAEDBChkZTli\nZmNjNGI5NzhhYmE3YjBiYmM3Y2FjMGZlM2RkZmYyZWFlYzc1MBkGCisGAQQBg78w\nAQQEC1ByZS1SZWxlYXNlMDUGCisGAQQBg78wAQUEJ2F3cy1wb3dlcnRvb2xzL3Bv\nd2VydG9vbHMtbGFtYmRhLXB5dGhvbjAgBgorBgEEAYO/MAEGBBJyZWZzL2hlYWRz\nL2RldmVsb3AwOwYKKwYBBAGDvzABCAQtDCtodHRwczovL3Rva2VuLmFjdGlvbnMu\nZ2l0aHVidXNlcmNvbnRlbnQuY29tMIGGBgorBgEEAYO/MAEJBHgMdmh0dHBzOi8v\nZ2l0aHViLmNvbS9zbHNhLWZyYW1ld29yay9zbHNhLWdpdGh1Yi1nZW5lcmF0b3Iv\nLmdpdGh1Yi93b3JrZmxvd3MvZ2VuZXJhdG9yX2dlbmVyaWNfc2xzYTMueW1sQHJl\nZnMvdGFncy92Mi4wLjAwOAYKKwYBBAGDvzABCgQqDCg1YTc3NWIzNjdhNTZkNWJk\nMTE4YTIyNGE4MTFiYmEyODgxNTBhNTYzMB0GCisGAQQBg78wAQsEDwwNZ2l0aHVi\nLWhvc3RlZDBKBgorBgEEAYO/MAEMBDwMOmh0dHBzOi8vZ2l0aHViLmNvbS9hd3Mt\ncG93ZXJ0b29scy9wb3dlcnRvb2xzLWxhbWJkYS1weXRob24wOAYKKwYBBAGDvzAB\nDQQqDChkZTliZmNjNGI5NzhhYmE3YjBiYmM3Y2FjMGZlM2RkZmYyZWFlYzc1MCIG\nCisGAQQBg78wAQ4EFAwScmVmcy9oZWFkcy9kZXZlbG9wMBkGCisGAQQBg78wAQ8E\nCwwJMjIxOTE5Mzc5MDEGCisGAQQBg78wARAEIwwhaHR0cHM6Ly9naXRodWIuY29t\nL2F3cy1wb3dlcnRvb2xzMBkGCisGAQQBg78wAREECwwJMTI5MTI3NjM4MH8GCisG\nAQQBg78wARIEcQxvaHR0cHM6Ly9naXRodWIuY29tL2F3cy1wb3dlcnRvb2xzL3Bv\nd2VydG9vbHMtbGFtYmRhLXB5dGhvbi8uZ2l0aHViL3dvcmtmbG93cy9wcmUtcmVs\nZWFzZS55bWxAcmVmcy9oZWFkcy9kZXZlbG9wMDgGCisGAQQBg78wARMEKgwoZGU5\nYmZjYzRiOTc4YWJhN2IwYmJjN2NhYzBmZTNkZGZmMmVhZWM3NTAYBgorBgEEAYO/\nMAEUBAoMCHNjaGVkdWxlMG0GCisGAQQBg78wARUEXwxdaHR0cHM6Ly9naXRodWIu\nY29tL2F3cy1wb3dlcnRvb2xzL3Bvd2VydG9vbHMtbGFtYmRhLXB5dGhvbi9hY3Rp\nb25zL3J1bnMvOTYxMDMyMDE5My9hdHRlbXB0cy8xMBYGCisGAQQBg78wARYECAwG\ncHVibGljMIGKBgorBgEEAdZ5AgQCBHwEegB4AHYA3T0wasbHETJjGR4cmWc3AqJK\nXrjePK3/h4pygC8p7o4AAAGQOdbVjgAABAMARzBFAiEAyFjoksT8EfHzSKyoHDhq\nb7JCvgQcKVd49EwdH12o4I8CIAZMFtXEpTVwf4jlvDXbEGvcOK3oJoz+T8geEoFG\n9/pbMAoGCCqGSM49BAMDA2gAMGUCMBkQZJHfITjQanS37s6Zb8EcHpzydcDyUdRv\nUkXO3N4U6WPhsceyGu4VO/GS3yOmNQIxAMNCvQybYlKiFhrjjwSDZqCgKyx+qzmQ\nQRq0dmZWKmsU/GjCy4sbK2W3jtoS0sGv9g==\n-----END CERTIFICATE-----\n"}]}
\ No newline at end of file
diff --git a/provenance/2.39.2a2/multiple.intoto.jsonl b/provenance/2.39.2a2/multiple.intoto.jsonl
new file mode 100644
index 00000000000..9c06657ffdd
--- /dev/null
+++ b/provenance/2.39.2a2/multiple.intoto.jsonl
@@ -0,0 +1 @@
+{"payloadType":"application/vnd.in-toto+json","payload":"eyJfdHlwZSI6Imh0dHBzOi8vaW4tdG90by5pby9TdGF0ZW1lbnQvdjAuMSIsInByZWRpY2F0ZVR5cGUiOiJodHRwczovL3Nsc2EuZGV2L3Byb3ZlbmFuY2UvdjAuMiIsInN1YmplY3QiOlt7Im5hbWUiOiIuL2F3c19sYW1iZGFfcG93ZXJ0b29scy0yLjM5LjJhMi1weTMtbm9uZS1hbnkud2hsIiwiZGlnZXN0Ijp7InNoYTI1NiI6IjdmYmM2MDU4NTVhNWQyYmZlZjQxZjYwMmFkMTAwYzQ4ZDA1MGRjZjc1MGEwYWI5NzVlZjU5MzBjOGE5NDI3ZGMifX0seyJuYW1lIjoiLi9hd3NfbGFtYmRhX3Bvd2VydG9vbHMtMi4zOS4yYTIudGFyLmd6IiwiZGlnZXN0Ijp7InNoYTI1NiI6IjgwMmEzYTc4MjVlOWMyYmFkMTNkOGMxMjA4OWZkZTA3ODU5M2EzZWU1ZmMxNjRhZjBiMzRmOWUzYWYwZjQxZTUifX1dLCJwcmVkaWNhdGUiOnsiYnVpbGRlciI6eyJpZCI6Imh0dHBzOi8vZ2l0aHViLmNvbS9zbHNhLWZyYW1ld29yay9zbHNhLWdpdGh1Yi1nZW5lcmF0b3IvLmdpdGh1Yi93b3JrZmxvd3MvZ2VuZXJhdG9yX2dlbmVyaWNfc2xzYTMueW1sQHJlZnMvdGFncy92Mi4wLjAifSwiYnVpbGRUeXBlIjoiaHR0cHM6Ly9naXRodWIuY29tL3Nsc2EtZnJhbWV3b3JrL3Nsc2EtZ2l0aHViLWdlbmVyYXRvci9nZW5lcmljQHYxIiwiaW52b2NhdGlvbiI6eyJjb25maWdTb3VyY2UiOnsidXJpIjoiZ2l0K2h0dHBzOi8vZ2l0aHViLmNvbS9hd3MtcG93ZXJ0b29scy9wb3dlcnRvb2xzLWxhbWJkYS1weXRob25AcmVmcy9oZWFkcy9kZXZlbG9wIiwiZGlnZXN0Ijp7InNoYTEiOiJiZDgwY2Y0NjI2ZDk5ZDdmMDY3Y2NiOWFhODFlOTY3ZTY0NDNjMTFkIn0sImVudHJ5UG9pbnQiOiIuZ2l0aHViL3dvcmtmbG93cy9wcmUtcmVsZWFzZS55bWwifSwicGFyYW1ldGVycyI6e30sImVudmlyb25tZW50Ijp7ImdpdGh1Yl9hY3RvciI6ImxlYW5kcm9kYW1hc2NlbmEiLCJnaXRodWJfYWN0b3JfaWQiOiI0Mjk1MTczIiwiZ2l0aHViX2Jhc2VfcmVmIjoiIiwiZ2l0aHViX2V2ZW50X25hbWUiOiJzY2hlZHVsZSIsImdpdGh1Yl9ldmVudF9wYXlsb2FkIjp7ImVudGVycHJpc2UiOnsiYXZhdGFyX3VybCI6Imh0dHBzOi8vYXZhdGFycy5naXRodWJ1c2VyY29udGVudC5jb20vYi8xMjkwP3Y9NCIsImNyZWF0ZWRfYXQiOiIyMDE5LTExLTEzVDE4OjA1OjQxWiIsImRlc2NyaXB0aW9uIjoiIiwiaHRtbF91cmwiOiJodHRwczovL2dpdGh1Yi5jb20vZW50ZXJwcmlzZXMvYW1hem9uIiwiaWQiOjEyOTAsIm5hbWUiOiJBbWF6b24iLCJub2RlX2lkIjoiTURFd09rVnVkR1Z5Y0hKcGMyVXhNamt3Iiwic2x1ZyI6ImFtYXpvbiIsInVwZGF0ZWRfYXQiOiIyMDIzLTAxLTI3VDE0OjU2OjEwWiIsIndlYnNpdGVfdXJsIjoiaHR0cHM6Ly93d3cuYW1hem9uLmNvbS8ifSwib3JnYW5pemF0aW9uIjp7ImF2YXRhcl91cmwiOiJodHRwczovL2F2YXRhcnMuZ2l0aHVidXNlcmNvbnRlbnQuY29tL3UvMTI5MTI3NjM4P3Y9NCIsImRlc2NyaXB0aW9uIjoiIiwiZXZlbnRzX3VybCI6Imh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vb3Jncy9hd3MtcG93ZXJ0b29scy9ldmVudHMiLCJob29rc191cmwiOiJodHRwczovL2FwaS5naXRodWIuY29tL29yZ3MvYXdzLXBvd2VydG9vbHMvaG9va3MiLCJpZCI6MTI5MTI3NjM4LCJpc3N1ZXNfdXJsIjoiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9vcmdzL2F3cy1wb3dlcnRvb2xzL2lzc3VlcyIsImxvZ2luIjoiYXdzLXBvd2VydG9vbHMiLCJtZW1iZXJzX3VybCI6Imh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vb3Jncy9hd3MtcG93ZXJ0b29scy9tZW1iZXJzey9tZW1iZXJ9Iiwibm9kZV9pZCI6Ik9fa2dET0I3SlUxZyIsInB1YmxpY19tZW1iZXJzX3VybCI6Imh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vb3Jncy9hd3MtcG93ZXJ0b29scy9wdWJsaWNfbWVtYmVyc3svbWVtYmVyfSIsInJlcG9zX3VybCI6Imh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vb3Jncy9hd3MtcG93ZXJ0b29scy9yZXBvcyIsInVybCI6Imh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vb3Jncy9hd3MtcG93ZXJ0b29scyJ9LCJyZXBvc2l0b3J5Ijp7ImFsbG93X2ZvcmtpbmciOnRydWUsImFyY2hpdmVfdXJsIjoiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9hd3MtcG93ZXJ0b29scy9wb3dlcnRvb2xzLWxhbWJkYS1weXRob24ve2FyY2hpdmVfZm9ybWF0fXsvcmVmfSIsImFyY2hpdmVkIjpmYWxzZSwiYXNzaWduZWVzX3VybCI6Imh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYXdzLXBvd2VydG9vbHMvcG93ZXJ0b29scy1sYW1iZGEtcHl0aG9uL2Fzc2lnbmVlc3svdXNlcn0iLCJibG9ic191cmwiOiJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2F3cy1wb3dlcnRvb2xzL3Bvd2VydG9vbHMtbGFtYmRhLXB5dGhvbi9naXQvYmxvYnN7L3NoYX0iLCJicmFuY2hlc191cmwiOiJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2F3cy1wb3dlcnRvb2xzL3Bvd2VydG9vbHMtbGFtYmRhLXB5dGhvbi9icmFuY2hlc3svYnJhbmNofSIsImNsb25lX3VybCI6Imh0dHBzOi8vZ2l0aHViLmNvbS9hd3MtcG93ZXJ0b29scy9wb3dlcnRvb2xzLWxhbWJkYS1weXRob24uZ2l0IiwiY29sbGFib3JhdG9yc191cmwiOiJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2F3cy1wb3dlcnRvb2xzL3Bvd2VydG9vbHMtbGFtYmRhLXB5dGhvbi9jb2xsYWJvcmF0b3Jzey9jb2xsYWJvcmF0b3J9IiwiY29tbWVudHNfdXJsIjoiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9hd3MtcG93ZXJ0b29scy9wb3dlcnRvb2xzLWxhbWJkYS1weXRob24vY29tbWVudHN7L251bWJlcn0iLCJjb21taXRzX3VybCI6Imh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYXdzLXBvd2VydG9vbHMvcG93ZXJ0b29scy1sYW1iZGEtcHl0aG9uL2NvbW1pdHN7L3NoYX0iLCJjb21wYXJlX3VybCI6Imh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYXdzLXBvd2VydG9vbHMvcG93ZXJ0b29scy1sYW1iZGEtcHl0aG9uL2NvbXBhcmUve2Jhc2V9Li4ue2hlYWR9IiwiY29udGVudHNfdXJsIjoiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9hd3MtcG93ZXJ0b29scy9wb3dlcnRvb2xzLWxhbWJkYS1weXRob24vY29udGVudHMveytwYXRofSIsImNvbnRyaWJ1dG9yc191cmwiOiJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2F3cy1wb3dlcnRvb2xzL3Bvd2VydG9vbHMtbGFtYmRhLXB5dGhvbi9jb250cmlidXRvcnMiLCJjcmVhdGVkX2F0IjoiMjAxOS0xMS0xNVQxMjoyNjoxMloiLCJkZWZhdWx0X2JyYW5jaCI6ImRldmVsb3AiLCJkZXBsb3ltZW50c191cmwiOiJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2F3cy1wb3dlcnRvb2xzL3Bvd2VydG9vbHMtbGFtYmRhLXB5dGhvbi9kZXBsb3ltZW50cyIsImRlc2NyaXB0aW9uIjoiQSBkZXZlbG9wZXIgdG9vbGtpdCB0byBpbXBsZW1lbnQgU2VydmVybGVzcyBiZXN0IHByYWN0aWNlcyBhbmQgaW5jcmVhc2UgZGV2ZWxvcGVyIHZlbG9jaXR5LiIsImRpc2FibGVkIjpmYWxzZSwiZG93bmxvYWRzX3VybCI6Imh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYXdzLXBvd2VydG9vbHMvcG93ZXJ0b29scy1sYW1iZGEtcHl0aG9uL2Rvd25sb2FkcyIsImV2ZW50c191cmwiOiJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2F3cy1wb3dlcnRvb2xzL3Bvd2VydG9vbHMtbGFtYmRhLXB5dGhvbi9ldmVudHMiLCJmb3JrIjpmYWxzZSwiZm9ya3MiOjM3NSwiZm9ya3NfY291bnQiOjM3NSwiZm9ya3NfdXJsIjoiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9hd3MtcG93ZXJ0b29scy9wb3dlcnRvb2xzLWxhbWJkYS1weXRob24vZm9ya3MiLCJmdWxsX25hbWUiOiJhd3MtcG93ZXJ0b29scy9wb3dlcnRvb2xzLWxhbWJkYS1weXRob24iLCJnaXRfY29tbWl0c191cmwiOiJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2F3cy1wb3dlcnRvb2xzL3Bvd2VydG9vbHMtbGFtYmRhLXB5dGhvbi9naXQvY29tbWl0c3svc2hhfSIsImdpdF9yZWZzX3VybCI6Imh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYXdzLXBvd2VydG9vbHMvcG93ZXJ0b29scy1sYW1iZGEtcHl0aG9uL2dpdC9yZWZzey9zaGF9IiwiZ2l0X3RhZ3NfdXJsIjoiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9hd3MtcG93ZXJ0b29scy9wb3dlcnRvb2xzLWxhbWJkYS1weXRob24vZ2l0L3RhZ3N7L3NoYX0iLCJnaXRfdXJsIjoiZ2l0Oi8vZ2l0aHViLmNvbS9hd3MtcG93ZXJ0b29scy9wb3dlcnRvb2xzLWxhbWJkYS1weXRob24uZ2l0IiwiaGFzX2Rpc2N1c3Npb25zIjp0cnVlLCJoYXNfZG93bmxvYWRzIjp0cnVlLCJoYXNfaXNzdWVzIjp0cnVlLCJoYXNfcGFnZXMiOmZhbHNlLCJoYXNfcHJvamVjdHMiOnRydWUsImhhc193aWtpIjpmYWxzZSwiaG9tZXBhZ2UiOiJodHRwczovL2RvY3MucG93ZXJ0b29scy5hd3MuZGV2L2xhbWJkYS9weXRob24vbGF0ZXN0LyIsImhvb2tzX3VybCI6Imh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYXdzLXBvd2VydG9vbHMvcG93ZXJ0b29scy1sYW1iZGEtcHl0aG9uL2hvb2tzIiwiaHRtbF91cmwiOiJodHRwczovL2dpdGh1Yi5jb20vYXdzLXBvd2VydG9vbHMvcG93ZXJ0b29scy1sYW1iZGEtcHl0aG9uIiwiaWQiOjIyMTkxOTM3OSwiaXNfdGVtcGxhdGUiOmZhbHNlLCJpc3N1ZV9jb21tZW50X3VybCI6Imh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYXdzLXBvd2VydG9vbHMvcG93ZXJ0b29scy1sYW1iZGEtcHl0aG9uL2lzc3Vlcy9jb21tZW50c3svbnVtYmVyfSIsImlzc3VlX2V2ZW50c191cmwiOiJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2F3cy1wb3dlcnRvb2xzL3Bvd2VydG9vbHMtbGFtYmRhLXB5dGhvbi9pc3N1ZXMvZXZlbnRzey9udW1iZXJ9IiwiaXNzdWVzX3VybCI6Imh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYXdzLXBvd2VydG9vbHMvcG93ZXJ0b29scy1sYW1iZGEtcHl0aG9uL2lzc3Vlc3svbnVtYmVyfSIsImtleXNfdXJsIjoiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9hd3MtcG93ZXJ0b29scy9wb3dlcnRvb2xzLWxhbWJkYS1weXRob24va2V5c3sva2V5X2lkfSIsImxhYmVsc191cmwiOiJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2F3cy1wb3dlcnRvb2xzL3Bvd2VydG9vbHMtbGFtYmRhLXB5dGhvbi9sYWJlbHN7L25hbWV9IiwibGFuZ3VhZ2UiOiJQeXRob24iLCJsYW5ndWFnZXNfdXJsIjoiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9hd3MtcG93ZXJ0b29scy9wb3dlcnRvb2xzLWxhbWJkYS1weXRob24vbGFuZ3VhZ2VzIiwibGljZW5zZSI6eyJrZXkiOiJtaXQtMCIsIm5hbWUiOiJNSVQgTm8gQXR0cmlidXRpb24iLCJub2RlX2lkIjoiTURjNlRHbGpaVzV6WlRReCIsInNwZHhfaWQiOiJNSVQtMCIsInVybCI6Imh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vbGljZW5zZXMvbWl0LTAifSwibWVyZ2VzX3VybCI6Imh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYXdzLXBvd2VydG9vbHMvcG93ZXJ0b29scy1sYW1iZGEtcHl0aG9uL21lcmdlcyIsIm1pbGVzdG9uZXNfdXJsIjoiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9hd3MtcG93ZXJ0b29scy9wb3dlcnRvb2xzLWxhbWJkYS1weXRob24vbWlsZXN0b25lc3svbnVtYmVyfSIsIm1pcnJvcl91cmwiOm51bGwsIm5hbWUiOiJwb3dlcnRvb2xzLWxhbWJkYS1weXRob24iLCJub2RlX2lkIjoiTURFd09sSmxjRzl6YVhSdmNua3lNakU1TVRrek56az0iLCJub3RpZmljYXRpb25zX3VybCI6Imh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYXdzLXBvd2VydG9vbHMvcG93ZXJ0b29scy1sYW1iZGEtcHl0aG9uL25vdGlmaWNhdGlvbnN7P3NpbmNlLGFsbCxwYXJ0aWNpcGF0aW5nfSIsIm9wZW5faXNzdWVzIjo5Nywib3Blbl9pc3N1ZXNfY291bnQiOjk3LCJvd25lciI6eyJhdmF0YXJfdXJsIjoiaHR0cHM6Ly9hdmF0YXJzLmdpdGh1YnVzZXJjb250ZW50LmNvbS91LzEyOTEyNzYzOD92PTQiLCJldmVudHNfdXJsIjoiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS91c2Vycy9hd3MtcG93ZXJ0b29scy9ldmVudHN7L3ByaXZhY3l9IiwiZm9sbG93ZXJzX3VybCI6Imh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vdXNlcnMvYXdzLXBvd2VydG9vbHMvZm9sbG93ZXJzIiwiZm9sbG93aW5nX3VybCI6Imh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vdXNlcnMvYXdzLXBvd2VydG9vbHMvZm9sbG93aW5ney9vdGhlcl91c2VyfSIsImdpc3RzX3VybCI6Imh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vdXNlcnMvYXdzLXBvd2VydG9vbHMvZ2lzdHN7L2dpc3RfaWR9IiwiZ3JhdmF0YXJfaWQiOiIiLCJodG1sX3VybCI6Imh0dHBzOi8vZ2l0aHViLmNvbS9hd3MtcG93ZXJ0b29scyIsImlkIjoxMjkxMjc2MzgsImxvZ2luIjoiYXdzLXBvd2VydG9vbHMiLCJub2RlX2lkIjoiT19rZ0RPQjdKVTFnIiwib3JnYW5pemF0aW9uc191cmwiOiJodHRwczovL2FwaS5naXRodWIuY29tL3VzZXJzL2F3cy1wb3dlcnRvb2xzL29yZ3MiLCJyZWNlaXZlZF9ldmVudHNfdXJsIjoiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS91c2Vycy9hd3MtcG93ZXJ0b29scy9yZWNlaXZlZF9ldmVudHMiLCJyZXBvc191cmwiOiJodHRwczovL2FwaS5naXRodWIuY29tL3VzZXJzL2F3cy1wb3dlcnRvb2xzL3JlcG9zIiwic2l0ZV9hZG1pbiI6ZmFsc2UsInN0YXJyZWRfdXJsIjoiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS91c2Vycy9hd3MtcG93ZXJ0b29scy9zdGFycmVkey9vd25lcn17L3JlcG99Iiwic3Vic2NyaXB0aW9uc191cmwiOiJodHRwczovL2FwaS5naXRodWIuY29tL3VzZXJzL2F3cy1wb3dlcnRvb2xzL3N1YnNjcmlwdGlvbnMiLCJ0eXBlIjoiT3JnYW5pemF0aW9uIiwidXJsIjoiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS91c2Vycy9hd3MtcG93ZXJ0b29scyJ9LCJwcml2YXRlIjpmYWxzZSwicHVsbHNfdXJsIjoiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9hd3MtcG93ZXJ0b29scy9wb3dlcnRvb2xzLWxhbWJkYS1weXRob24vcHVsbHN7L251bWJlcn0iLCJwdXNoZWRfYXQiOiIyMDI0LTA2LTIzVDEwOjAzOjUxWiIsInJlbGVhc2VzX3VybCI6Imh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYXdzLXBvd2VydG9vbHMvcG93ZXJ0b29scy1sYW1iZGEtcHl0aG9uL3JlbGVhc2Vzey9pZH0iLCJzaXplIjo0NjMxNSwic3NoX3VybCI6ImdpdEBnaXRodWIuY29tOmF3cy1wb3dlcnRvb2xzL3Bvd2VydG9vbHMtbGFtYmRhLXB5dGhvbi5naXQiLCJzdGFyZ2F6ZXJzX2NvdW50IjoyNzA5LCJzdGFyZ2F6ZXJzX3VybCI6Imh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYXdzLXBvd2VydG9vbHMvcG93ZXJ0b29scy1sYW1iZGEtcHl0aG9uL3N0YXJnYXplcnMiLCJzdGF0dXNlc191cmwiOiJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2F3cy1wb3dlcnRvb2xzL3Bvd2VydG9vbHMtbGFtYmRhLXB5dGhvbi9zdGF0dXNlcy97c2hhfSIsInN1YnNjcmliZXJzX3VybCI6Imh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYXdzLXBvd2VydG9vbHMvcG93ZXJ0b29scy1sYW1iZGEtcHl0aG9uL3N1YnNjcmliZXJzIiwic3Vic2NyaXB0aW9uX3VybCI6Imh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYXdzLXBvd2VydG9vbHMvcG93ZXJ0b29scy1sYW1iZGEtcHl0aG9uL3N1YnNjcmlwdGlvbiIsInN2bl91cmwiOiJodHRwczovL2dpdGh1Yi5jb20vYXdzLXBvd2VydG9vbHMvcG93ZXJ0b29scy1sYW1iZGEtcHl0aG9uIiwidGFnc191cmwiOiJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2F3cy1wb3dlcnRvb2xzL3Bvd2VydG9vbHMtbGFtYmRhLXB5dGhvbi90YWdzIiwidGVhbXNfdXJsIjoiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9hd3MtcG93ZXJ0b29scy9wb3dlcnRvb2xzLWxhbWJkYS1weXRob24vdGVhbXMiLCJ0b3BpY3MiOlsiYXdzIiwiYXdzLWxhbWJkYSIsImhhY2t0b2JlcmZlc3QiLCJsYW1iZGEiLCJweXRob24iLCJzZXJ2ZXJsZXNzIl0sInRyZWVzX3VybCI6Imh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYXdzLXBvd2VydG9vbHMvcG93ZXJ0b29scy1sYW1iZGEtcHl0aG9uL2dpdC90cmVlc3svc2hhfSIsInVwZGF0ZWRfYXQiOiIyMDI0LTA2LTI0VDA3OjExOjIxWiIsInVybCI6Imh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYXdzLXBvd2VydG9vbHMvcG93ZXJ0b29scy1sYW1iZGEtcHl0aG9uIiwidmlzaWJpbGl0eSI6InB1YmxpYyIsIndhdGNoZXJzIjoyNzA5LCJ3YXRjaGVyc19jb3VudCI6MjcwOSwid2ViX2NvbW1pdF9zaWdub2ZmX3JlcXVpcmVkIjp0cnVlfSwic2NoZWR1bGUiOiIwIDggKiAqIDEtNSIsIndvcmtmbG93IjoiLmdpdGh1Yi93b3JrZmxvd3MvcHJlLXJlbGVhc2UueW1sIn0sImdpdGh1Yl9oZWFkX3JlZiI6IiIsImdpdGh1Yl9yZWYiOiJyZWZzL2hlYWRzL2RldmVsb3AiLCJnaXRodWJfcmVmX3R5cGUiOiJicmFuY2giLCJnaXRodWJfcmVwb3NpdG9yeV9pZCI6IjIyMTkxOTM3OSIsImdpdGh1Yl9yZXBvc2l0b3J5X293bmVyIjoiYXdzLXBvd2VydG9vbHMiLCJnaXRodWJfcmVwb3NpdG9yeV9vd25lcl9pZCI6IjEyOTEyNzYzOCIsImdpdGh1Yl9ydW5fYXR0ZW1wdCI6IjEiLCJnaXRodWJfcnVuX2lkIjoiOTY0MTc0MDA1NyIsImdpdGh1Yl9ydW5fbnVtYmVyIjoiMyIsImdpdGh1Yl9zaGExIjoiYmQ4MGNmNDYyNmQ5OWQ3ZjA2N2NjYjlhYTgxZTk2N2U2NDQzYzExZCJ9fSwibWV0YWRhdGEiOnsiYnVpbGRJbnZvY2F0aW9uSUQiOiI5NjQxNzQwMDU3LTEiLCJjb21wbGV0ZW5lc3MiOnsicGFyYW1ldGVycyI6dHJ1ZSwiZW52aXJvbm1lbnQiOmZhbHNlLCJtYXRlcmlhbHMiOmZhbHNlfSwicmVwcm9kdWNpYmxlIjpmYWxzZX0sIm1hdGVyaWFscyI6W3sidXJpIjoiZ2l0K2h0dHBzOi8vZ2l0aHViLmNvbS9hd3MtcG93ZXJ0b29scy9wb3dlcnRvb2xzLWxhbWJkYS1weXRob25AcmVmcy9oZWFkcy9kZXZlbG9wIiwiZGlnZXN0Ijp7InNoYTEiOiJiZDgwY2Y0NjI2ZDk5ZDdmMDY3Y2NiOWFhODFlOTY3ZTY0NDNjMTFkIn19XX19","signatures":[{"keyid":"","sig":"MEUCIBn5AhbydV+cFbI1gZGslqr46YyNsrUYSl0Qii4/PY1UAiEA/K+B9N73QdH0KQiZEKC8h/6VMcqTRYCCYtXZG/AxmwY=","cert":"-----BEGIN CERTIFICATE-----\nMIIHZTCCBuugAwIBAgIUPDKAct6EIbz3gD8s2RiFRJKAPlwwCgYIKoZIzj0EAwMw\nNzEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MR4wHAYDVQQDExVzaWdzdG9yZS1pbnRl\ncm1lZGlhdGUwHhcNMjQwNjI0MDgwNzU4WhcNMjQwNjI0MDgxNzU4WjAAMFkwEwYH\nKoZIzj0CAQYIKoZIzj0DAQcDQgAEZMYFSDIqkkFDOsSIPndvimm2LSC0CWgpOhqC\neRUSGEl7hbm4zxDSb0rUp/tcBjrKISxuluSoA6WU63XTXl0JfqOCBgowggYGMA4G\nA1UdDwEB/wQEAwIHgDATBgNVHSUEDDAKBggrBgEFBQcDAzAdBgNVHQ4EFgQU8IvA\nGBVeJkoj3MQhInO8pUnta0wwHwYDVR0jBBgwFoAU39Ppz1YkEZb5qNjpKFWixi4Y\nZD8wgYQGA1UdEQEB/wR6MHiGdmh0dHBzOi8vZ2l0aHViLmNvbS9zbHNhLWZyYW1l\nd29yay9zbHNhLWdpdGh1Yi1nZW5lcmF0b3IvLmdpdGh1Yi93b3JrZmxvd3MvZ2Vu\nZXJhdG9yX2dlbmVyaWNfc2xzYTMueW1sQHJlZnMvdGFncy92Mi4wLjAwOQYKKwYB\nBAGDvzABAQQraHR0cHM6Ly90b2tlbi5hY3Rpb25zLmdpdGh1YnVzZXJjb250ZW50\nLmNvbTAWBgorBgEEAYO/MAECBAhzY2hlZHVsZTA2BgorBgEEAYO/MAEDBChiZDgw\nY2Y0NjI2ZDk5ZDdmMDY3Y2NiOWFhODFlOTY3ZTY0NDNjMTFkMBkGCisGAQQBg78w\nAQQEC1ByZS1SZWxlYXNlMDUGCisGAQQBg78wAQUEJ2F3cy1wb3dlcnRvb2xzL3Bv\nd2VydG9vbHMtbGFtYmRhLXB5dGhvbjAgBgorBgEEAYO/MAEGBBJyZWZzL2hlYWRz\nL2RldmVsb3AwOwYKKwYBBAGDvzABCAQtDCtodHRwczovL3Rva2VuLmFjdGlvbnMu\nZ2l0aHVidXNlcmNvbnRlbnQuY29tMIGGBgorBgEEAYO/MAEJBHgMdmh0dHBzOi8v\nZ2l0aHViLmNvbS9zbHNhLWZyYW1ld29yay9zbHNhLWdpdGh1Yi1nZW5lcmF0b3Iv\nLmdpdGh1Yi93b3JrZmxvd3MvZ2VuZXJhdG9yX2dlbmVyaWNfc2xzYTMueW1sQHJl\nZnMvdGFncy92Mi4wLjAwOAYKKwYBBAGDvzABCgQqDCg1YTc3NWIzNjdhNTZkNWJk\nMTE4YTIyNGE4MTFiYmEyODgxNTBhNTYzMB0GCisGAQQBg78wAQsEDwwNZ2l0aHVi\nLWhvc3RlZDBKBgorBgEEAYO/MAEMBDwMOmh0dHBzOi8vZ2l0aHViLmNvbS9hd3Mt\ncG93ZXJ0b29scy9wb3dlcnRvb2xzLWxhbWJkYS1weXRob24wOAYKKwYBBAGDvzAB\nDQQqDChiZDgwY2Y0NjI2ZDk5ZDdmMDY3Y2NiOWFhODFlOTY3ZTY0NDNjMTFkMCIG\nCisGAQQBg78wAQ4EFAwScmVmcy9oZWFkcy9kZXZlbG9wMBkGCisGAQQBg78wAQ8E\nCwwJMjIxOTE5Mzc5MDEGCisGAQQBg78wARAEIwwhaHR0cHM6Ly9naXRodWIuY29t\nL2F3cy1wb3dlcnRvb2xzMBkGCisGAQQBg78wAREECwwJMTI5MTI3NjM4MH8GCisG\nAQQBg78wARIEcQxvaHR0cHM6Ly9naXRodWIuY29tL2F3cy1wb3dlcnRvb2xzL3Bv\nd2VydG9vbHMtbGFtYmRhLXB5dGhvbi8uZ2l0aHViL3dvcmtmbG93cy9wcmUtcmVs\nZWFzZS55bWxAcmVmcy9oZWFkcy9kZXZlbG9wMDgGCisGAQQBg78wARMEKgwoYmQ4\nMGNmNDYyNmQ5OWQ3ZjA2N2NjYjlhYTgxZTk2N2U2NDQzYzExZDAYBgorBgEEAYO/\nMAEUBAoMCHNjaGVkdWxlMG0GCisGAQQBg78wARUEXwxdaHR0cHM6Ly9naXRodWIu\nY29tL2F3cy1wb3dlcnRvb2xzL3Bvd2VydG9vbHMtbGFtYmRhLXB5dGhvbi9hY3Rp\nb25zL3J1bnMvOTY0MTc0MDA1Ny9hdHRlbXB0cy8xMBYGCisGAQQBg78wARYECAwG\ncHVibGljMIGKBgorBgEEAdZ5AgQCBHwEegB4AHYA3T0wasbHETJjGR4cmWc3AqJK\nXrjePK3/h4pygC8p7o4AAAGQSUmZFAAABAMARzBFAiBYScEI7kAo4zk7yqQDvZ5c\nAfx5s6R8j3NFdE2EyDVTcQIhAMAFRCBxK3eDrk1KcpD4R2SiCMZKbTIphj4jt5qV\nuTcfMAoGCCqGSM49BAMDA2gAMGUCMAUGr1byXm39wn9OqbQIER9aIWiU9gdzZ4Mz\ndsMbNpOig20G5euqf4ztoRVLiQ2+cwIxANEd2Gg/a22OhI/RXvsmF8bw5R6kX3SV\nTMj1MEQrjTJHMlY21StiEwJ0UPCdyiyWdw==\n-----END CERTIFICATE-----\n"}]}
\ No newline at end of file
diff --git a/provenance/2.39.2a3/multiple.intoto.jsonl b/provenance/2.39.2a3/multiple.intoto.jsonl
new file mode 100644
index 00000000000..75e63d441b7
--- /dev/null
+++ b/provenance/2.39.2a3/multiple.intoto.jsonl
@@ -0,0 +1 @@
+{"payloadType":"application/vnd.in-toto+json","payload":"eyJfdHlwZSI6Imh0dHBzOi8vaW4tdG90by5pby9TdGF0ZW1lbnQvdjAuMSIsInByZWRpY2F0ZVR5cGUiOiJodHRwczovL3Nsc2EuZGV2L3Byb3ZlbmFuY2UvdjAuMiIsInN1YmplY3QiOlt7Im5hbWUiOiIuL2F3c19sYW1iZGFfcG93ZXJ0b29scy0yLjM5LjJhMy1weTMtbm9uZS1hbnkud2hsIiwiZGlnZXN0Ijp7InNoYTI1NiI6IjM5MmQ1YTZjM2QwZjI3YTI0NzBhMzQ2M2VjMDc3ODIzMDgzMmU0MWM1MDdhZjUyODJjMzZkZWEwYzQyNzBkOTMifX0seyJuYW1lIjoiLi9hd3NfbGFtYmRhX3Bvd2VydG9vbHMtMi4zOS4yYTMudGFyLmd6IiwiZGlnZXN0Ijp7InNoYTI1NiI6IjJjODM4MGRlM2EyN2EwMjM1MDFiOTExODUwMzYyM2FmOGEzYzk5NDMwM2M2NmEyYzRhZjhhMzA0ZjY3Nzg3MDYifX1dLCJwcmVkaWNhdGUiOnsiYnVpbGRlciI6eyJpZCI6Imh0dHBzOi8vZ2l0aHViLmNvbS9zbHNhLWZyYW1ld29yay9zbHNhLWdpdGh1Yi1nZW5lcmF0b3IvLmdpdGh1Yi93b3JrZmxvd3MvZ2VuZXJhdG9yX2dlbmVyaWNfc2xzYTMueW1sQHJlZnMvdGFncy92Mi4wLjAifSwiYnVpbGRUeXBlIjoiaHR0cHM6Ly9naXRodWIuY29tL3Nsc2EtZnJhbWV3b3JrL3Nsc2EtZ2l0aHViLWdlbmVyYXRvci9nZW5lcmljQHYxIiwiaW52b2NhdGlvbiI6eyJjb25maWdTb3VyY2UiOnsidXJpIjoiZ2l0K2h0dHBzOi8vZ2l0aHViLmNvbS9hd3MtcG93ZXJ0b29scy9wb3dlcnRvb2xzLWxhbWJkYS1weXRob25AcmVmcy9oZWFkcy9kZXZlbG9wIiwiZGlnZXN0Ijp7InNoYTEiOiI2M2Y1YjVjYzQ0YzVjY2FlNjY1MTM5N2MyZGVlNDUzMmRjYjM0NmNlIn0sImVudHJ5UG9pbnQiOiIuZ2l0aHViL3dvcmtmbG93cy9wcmUtcmVsZWFzZS55bWwifSwicGFyYW1ldGVycyI6e30sImVudmlyb25tZW50Ijp7ImdpdGh1Yl9hY3RvciI6ImxlYW5kcm9kYW1hc2NlbmEiLCJnaXRodWJfYWN0b3JfaWQiOiI0Mjk1MTczIiwiZ2l0aHViX2Jhc2VfcmVmIjoiIiwiZ2l0aHViX2V2ZW50X25hbWUiOiJzY2hlZHVsZSIsImdpdGh1Yl9ldmVudF9wYXlsb2FkIjp7ImVudGVycHJpc2UiOnsiYXZhdGFyX3VybCI6Imh0dHBzOi8vYXZhdGFycy5naXRodWJ1c2VyY29udGVudC5jb20vYi8xMjkwP3Y9NCIsImNyZWF0ZWRfYXQiOiIyMDE5LTExLTEzVDE4OjA1OjQxWiIsImRlc2NyaXB0aW9uIjoiIiwiaHRtbF91cmwiOiJodHRwczovL2dpdGh1Yi5jb20vZW50ZXJwcmlzZXMvYW1hem9uIiwiaWQiOjEyOTAsIm5hbWUiOiJBbWF6b24iLCJub2RlX2lkIjoiTURFd09rVnVkR1Z5Y0hKcGMyVXhNamt3Iiwic2x1ZyI6ImFtYXpvbiIsInVwZGF0ZWRfYXQiOiIyMDIzLTAxLTI3VDE0OjU2OjEwWiIsIndlYnNpdGVfdXJsIjoiaHR0cHM6Ly93d3cuYW1hem9uLmNvbS8ifSwib3JnYW5pemF0aW9uIjp7ImF2YXRhcl91cmwiOiJodHRwczovL2F2YXRhcnMuZ2l0aHVidXNlcmNvbnRlbnQuY29tL3UvMTI5MTI3NjM4P3Y9NCIsImRlc2NyaXB0aW9uIjoiIiwiZXZlbnRzX3VybCI6Imh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vb3Jncy9hd3MtcG93ZXJ0b29scy9ldmVudHMiLCJob29rc191cmwiOiJodHRwczovL2FwaS5naXRodWIuY29tL29yZ3MvYXdzLXBvd2VydG9vbHMvaG9va3MiLCJpZCI6MTI5MTI3NjM4LCJpc3N1ZXNfdXJsIjoiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9vcmdzL2F3cy1wb3dlcnRvb2xzL2lzc3VlcyIsImxvZ2luIjoiYXdzLXBvd2VydG9vbHMiLCJtZW1iZXJzX3VybCI6Imh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vb3Jncy9hd3MtcG93ZXJ0b29scy9tZW1iZXJzey9tZW1iZXJ9Iiwibm9kZV9pZCI6Ik9fa2dET0I3SlUxZyIsInB1YmxpY19tZW1iZXJzX3VybCI6Imh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vb3Jncy9hd3MtcG93ZXJ0b29scy9wdWJsaWNfbWVtYmVyc3svbWVtYmVyfSIsInJlcG9zX3VybCI6Imh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vb3Jncy9hd3MtcG93ZXJ0b29scy9yZXBvcyIsInVybCI6Imh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vb3Jncy9hd3MtcG93ZXJ0b29scyJ9LCJyZXBvc2l0b3J5Ijp7ImFsbG93X2ZvcmtpbmciOnRydWUsImFyY2hpdmVfdXJsIjoiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9hd3MtcG93ZXJ0b29scy9wb3dlcnRvb2xzLWxhbWJkYS1weXRob24ve2FyY2hpdmVfZm9ybWF0fXsvcmVmfSIsImFyY2hpdmVkIjpmYWxzZSwiYXNzaWduZWVzX3VybCI6Imh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYXdzLXBvd2VydG9vbHMvcG93ZXJ0b29scy1sYW1iZGEtcHl0aG9uL2Fzc2lnbmVlc3svdXNlcn0iLCJibG9ic191cmwiOiJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2F3cy1wb3dlcnRvb2xzL3Bvd2VydG9vbHMtbGFtYmRhLXB5dGhvbi9naXQvYmxvYnN7L3NoYX0iLCJicmFuY2hlc191cmwiOiJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2F3cy1wb3dlcnRvb2xzL3Bvd2VydG9vbHMtbGFtYmRhLXB5dGhvbi9icmFuY2hlc3svYnJhbmNofSIsImNsb25lX3VybCI6Imh0dHBzOi8vZ2l0aHViLmNvbS9hd3MtcG93ZXJ0b29scy9wb3dlcnRvb2xzLWxhbWJkYS1weXRob24uZ2l0IiwiY29sbGFib3JhdG9yc191cmwiOiJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2F3cy1wb3dlcnRvb2xzL3Bvd2VydG9vbHMtbGFtYmRhLXB5dGhvbi9jb2xsYWJvcmF0b3Jzey9jb2xsYWJvcmF0b3J9IiwiY29tbWVudHNfdXJsIjoiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9hd3MtcG93ZXJ0b29scy9wb3dlcnRvb2xzLWxhbWJkYS1weXRob24vY29tbWVudHN7L251bWJlcn0iLCJjb21taXRzX3VybCI6Imh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYXdzLXBvd2VydG9vbHMvcG93ZXJ0b29scy1sYW1iZGEtcHl0aG9uL2NvbW1pdHN7L3NoYX0iLCJjb21wYXJlX3VybCI6Imh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYXdzLXBvd2VydG9vbHMvcG93ZXJ0b29scy1sYW1iZGEtcHl0aG9uL2NvbXBhcmUve2Jhc2V9Li4ue2hlYWR9IiwiY29udGVudHNfdXJsIjoiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9hd3MtcG93ZXJ0b29scy9wb3dlcnRvb2xzLWxhbWJkYS1weXRob24vY29udGVudHMveytwYXRofSIsImNvbnRyaWJ1dG9yc191cmwiOiJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2F3cy1wb3dlcnRvb2xzL3Bvd2VydG9vbHMtbGFtYmRhLXB5dGhvbi9jb250cmlidXRvcnMiLCJjcmVhdGVkX2F0IjoiMjAxOS0xMS0xNVQxMjoyNjoxMloiLCJkZWZhdWx0X2JyYW5jaCI6ImRldmVsb3AiLCJkZXBsb3ltZW50c191cmwiOiJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2F3cy1wb3dlcnRvb2xzL3Bvd2VydG9vbHMtbGFtYmRhLXB5dGhvbi9kZXBsb3ltZW50cyIsImRlc2NyaXB0aW9uIjoiQSBkZXZlbG9wZXIgdG9vbGtpdCB0byBpbXBsZW1lbnQgU2VydmVybGVzcyBiZXN0IHByYWN0aWNlcyBhbmQgaW5jcmVhc2UgZGV2ZWxvcGVyIHZlbG9jaXR5LiIsImRpc2FibGVkIjpmYWxzZSwiZG93bmxvYWRzX3VybCI6Imh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYXdzLXBvd2VydG9vbHMvcG93ZXJ0b29scy1sYW1iZGEtcHl0aG9uL2Rvd25sb2FkcyIsImV2ZW50c191cmwiOiJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2F3cy1wb3dlcnRvb2xzL3Bvd2VydG9vbHMtbGFtYmRhLXB5dGhvbi9ldmVudHMiLCJmb3JrIjpmYWxzZSwiZm9ya3MiOjM3NiwiZm9ya3NfY291bnQiOjM3NiwiZm9ya3NfdXJsIjoiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9hd3MtcG93ZXJ0b29scy9wb3dlcnRvb2xzLWxhbWJkYS1weXRob24vZm9ya3MiLCJmdWxsX25hbWUiOiJhd3MtcG93ZXJ0b29scy9wb3dlcnRvb2xzLWxhbWJkYS1weXRob24iLCJnaXRfY29tbWl0c191cmwiOiJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2F3cy1wb3dlcnRvb2xzL3Bvd2VydG9vbHMtbGFtYmRhLXB5dGhvbi9naXQvY29tbWl0c3svc2hhfSIsImdpdF9yZWZzX3VybCI6Imh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYXdzLXBvd2VydG9vbHMvcG93ZXJ0b29scy1sYW1iZGEtcHl0aG9uL2dpdC9yZWZzey9zaGF9IiwiZ2l0X3RhZ3NfdXJsIjoiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9hd3MtcG93ZXJ0b29scy9wb3dlcnRvb2xzLWxhbWJkYS1weXRob24vZ2l0L3RhZ3N7L3NoYX0iLCJnaXRfdXJsIjoiZ2l0Oi8vZ2l0aHViLmNvbS9hd3MtcG93ZXJ0b29scy9wb3dlcnRvb2xzLWxhbWJkYS1weXRob24uZ2l0IiwiaGFzX2Rpc2N1c3Npb25zIjp0cnVlLCJoYXNfZG93bmxvYWRzIjp0cnVlLCJoYXNfaXNzdWVzIjp0cnVlLCJoYXNfcGFnZXMiOmZhbHNlLCJoYXNfcHJvamVjdHMiOnRydWUsImhhc193aWtpIjpmYWxzZSwiaG9tZXBhZ2UiOiJodHRwczovL2RvY3MucG93ZXJ0b29scy5hd3MuZGV2L2xhbWJkYS9weXRob24vbGF0ZXN0LyIsImhvb2tzX3VybCI6Imh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYXdzLXBvd2VydG9vbHMvcG93ZXJ0b29scy1sYW1iZGEtcHl0aG9uL2hvb2tzIiwiaHRtbF91cmwiOiJodHRwczovL2dpdGh1Yi5jb20vYXdzLXBvd2VydG9vbHMvcG93ZXJ0b29scy1sYW1iZGEtcHl0aG9uIiwiaWQiOjIyMTkxOTM3OSwiaXNfdGVtcGxhdGUiOmZhbHNlLCJpc3N1ZV9jb21tZW50X3VybCI6Imh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYXdzLXBvd2VydG9vbHMvcG93ZXJ0b29scy1sYW1iZGEtcHl0aG9uL2lzc3Vlcy9jb21tZW50c3svbnVtYmVyfSIsImlzc3VlX2V2ZW50c191cmwiOiJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2F3cy1wb3dlcnRvb2xzL3Bvd2VydG9vbHMtbGFtYmRhLXB5dGhvbi9pc3N1ZXMvZXZlbnRzey9udW1iZXJ9IiwiaXNzdWVzX3VybCI6Imh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYXdzLXBvd2VydG9vbHMvcG93ZXJ0b29scy1sYW1iZGEtcHl0aG9uL2lzc3Vlc3svbnVtYmVyfSIsImtleXNfdXJsIjoiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9hd3MtcG93ZXJ0b29scy9wb3dlcnRvb2xzLWxhbWJkYS1weXRob24va2V5c3sva2V5X2lkfSIsImxhYmVsc191cmwiOiJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2F3cy1wb3dlcnRvb2xzL3Bvd2VydG9vbHMtbGFtYmRhLXB5dGhvbi9sYWJlbHN7L25hbWV9IiwibGFuZ3VhZ2UiOiJQeXRob24iLCJsYW5ndWFnZXNfdXJsIjoiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9hd3MtcG93ZXJ0b29scy9wb3dlcnRvb2xzLWxhbWJkYS1weXRob24vbGFuZ3VhZ2VzIiwibGljZW5zZSI6eyJrZXkiOiJtaXQtMCIsIm5hbWUiOiJNSVQgTm8gQXR0cmlidXRpb24iLCJub2RlX2lkIjoiTURjNlRHbGpaVzV6WlRReCIsInNwZHhfaWQiOiJNSVQtMCIsInVybCI6Imh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vbGljZW5zZXMvbWl0LTAifSwibWVyZ2VzX3VybCI6Imh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYXdzLXBvd2VydG9vbHMvcG93ZXJ0b29scy1sYW1iZGEtcHl0aG9uL21lcmdlcyIsIm1pbGVzdG9uZXNfdXJsIjoiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9hd3MtcG93ZXJ0b29scy9wb3dlcnRvb2xzLWxhbWJkYS1weXRob24vbWlsZXN0b25lc3svbnVtYmVyfSIsIm1pcnJvcl91cmwiOm51bGwsIm5hbWUiOiJwb3dlcnRvb2xzLWxhbWJkYS1weXRob24iLCJub2RlX2lkIjoiTURFd09sSmxjRzl6YVhSdmNua3lNakU1TVRrek56az0iLCJub3RpZmljYXRpb25zX3VybCI6Imh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYXdzLXBvd2VydG9vbHMvcG93ZXJ0b29scy1sYW1iZGEtcHl0aG9uL25vdGlmaWNhdGlvbnN7P3NpbmNlLGFsbCxwYXJ0aWNpcGF0aW5nfSIsIm9wZW5faXNzdWVzIjo5OCwib3Blbl9pc3N1ZXNfY291bnQiOjk4LCJvd25lciI6eyJhdmF0YXJfdXJsIjoiaHR0cHM6Ly9hdmF0YXJzLmdpdGh1YnVzZXJjb250ZW50LmNvbS91LzEyOTEyNzYzOD92PTQiLCJldmVudHNfdXJsIjoiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS91c2Vycy9hd3MtcG93ZXJ0b29scy9ldmVudHN7L3ByaXZhY3l9IiwiZm9sbG93ZXJzX3VybCI6Imh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vdXNlcnMvYXdzLXBvd2VydG9vbHMvZm9sbG93ZXJzIiwiZm9sbG93aW5nX3VybCI6Imh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vdXNlcnMvYXdzLXBvd2VydG9vbHMvZm9sbG93aW5ney9vdGhlcl91c2VyfSIsImdpc3RzX3VybCI6Imh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vdXNlcnMvYXdzLXBvd2VydG9vbHMvZ2lzdHN7L2dpc3RfaWR9IiwiZ3JhdmF0YXJfaWQiOiIiLCJodG1sX3VybCI6Imh0dHBzOi8vZ2l0aHViLmNvbS9hd3MtcG93ZXJ0b29scyIsImlkIjoxMjkxMjc2MzgsImxvZ2luIjoiYXdzLXBvd2VydG9vbHMiLCJub2RlX2lkIjoiT19rZ0RPQjdKVTFnIiwib3JnYW5pemF0aW9uc191cmwiOiJodHRwczovL2FwaS5naXRodWIuY29tL3VzZXJzL2F3cy1wb3dlcnRvb2xzL29yZ3MiLCJyZWNlaXZlZF9ldmVudHNfdXJsIjoiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS91c2Vycy9hd3MtcG93ZXJ0b29scy9yZWNlaXZlZF9ldmVudHMiLCJyZXBvc191cmwiOiJodHRwczovL2FwaS5naXRodWIuY29tL3VzZXJzL2F3cy1wb3dlcnRvb2xzL3JlcG9zIiwic2l0ZV9hZG1pbiI6ZmFsc2UsInN0YXJyZWRfdXJsIjoiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS91c2Vycy9hd3MtcG93ZXJ0b29scy9zdGFycmVkey9vd25lcn17L3JlcG99Iiwic3Vic2NyaXB0aW9uc191cmwiOiJodHRwczovL2FwaS5naXRodWIuY29tL3VzZXJzL2F3cy1wb3dlcnRvb2xzL3N1YnNjcmlwdGlvbnMiLCJ0eXBlIjoiT3JnYW5pemF0aW9uIiwidXJsIjoiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS91c2Vycy9hd3MtcG93ZXJ0b29scyJ9LCJwcml2YXRlIjpmYWxzZSwicHVsbHNfdXJsIjoiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9hd3MtcG93ZXJ0b29scy9wb3dlcnRvb2xzLWxhbWJkYS1weXRob24vcHVsbHN7L251bWJlcn0iLCJwdXNoZWRfYXQiOiIyMDI0LTA2LTI1VDA3OjE3OjMwWiIsInJlbGVhc2VzX3VybCI6Imh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYXdzLXBvd2VydG9vbHMvcG93ZXJ0b29scy1sYW1iZGEtcHl0aG9uL3JlbGVhc2Vzey9pZH0iLCJzaXplIjo0NzAyOCwic3NoX3VybCI6ImdpdEBnaXRodWIuY29tOmF3cy1wb3dlcnRvb2xzL3Bvd2VydG9vbHMtbGFtYmRhLXB5dGhvbi5naXQiLCJzdGFyZ2F6ZXJzX2NvdW50IjoyNzExLCJzdGFyZ2F6ZXJzX3VybCI6Imh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYXdzLXBvd2VydG9vbHMvcG93ZXJ0b29scy1sYW1iZGEtcHl0aG9uL3N0YXJnYXplcnMiLCJzdGF0dXNlc191cmwiOiJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2F3cy1wb3dlcnRvb2xzL3Bvd2VydG9vbHMtbGFtYmRhLXB5dGhvbi9zdGF0dXNlcy97c2hhfSIsInN1YnNjcmliZXJzX3VybCI6Imh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYXdzLXBvd2VydG9vbHMvcG93ZXJ0b29scy1sYW1iZGEtcHl0aG9uL3N1YnNjcmliZXJzIiwic3Vic2NyaXB0aW9uX3VybCI6Imh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYXdzLXBvd2VydG9vbHMvcG93ZXJ0b29scy1sYW1iZGEtcHl0aG9uL3N1YnNjcmlwdGlvbiIsInN2bl91cmwiOiJodHRwczovL2dpdGh1Yi5jb20vYXdzLXBvd2VydG9vbHMvcG93ZXJ0b29scy1sYW1iZGEtcHl0aG9uIiwidGFnc191cmwiOiJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2F3cy1wb3dlcnRvb2xzL3Bvd2VydG9vbHMtbGFtYmRhLXB5dGhvbi90YWdzIiwidGVhbXNfdXJsIjoiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9hd3MtcG93ZXJ0b29scy9wb3dlcnRvb2xzLWxhbWJkYS1weXRob24vdGVhbXMiLCJ0b3BpY3MiOlsiYXdzIiwiYXdzLWxhbWJkYSIsImhhY2t0b2JlcmZlc3QiLCJsYW1iZGEiLCJweXRob24iLCJzZXJ2ZXJsZXNzIl0sInRyZWVzX3VybCI6Imh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYXdzLXBvd2VydG9vbHMvcG93ZXJ0b29scy1sYW1iZGEtcHl0aG9uL2dpdC90cmVlc3svc2hhfSIsInVwZGF0ZWRfYXQiOiIyMDI0LTA2LTI0VDIzOjQ2OjIxWiIsInVybCI6Imh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYXdzLXBvd2VydG9vbHMvcG93ZXJ0b29scy1sYW1iZGEtcHl0aG9uIiwidmlzaWJpbGl0eSI6InB1YmxpYyIsIndhdGNoZXJzIjoyNzExLCJ3YXRjaGVyc19jb3VudCI6MjcxMSwid2ViX2NvbW1pdF9zaWdub2ZmX3JlcXVpcmVkIjp0cnVlfSwic2NoZWR1bGUiOiIwIDggKiAqIDEtNSIsIndvcmtmbG93IjoiLmdpdGh1Yi93b3JrZmxvd3MvcHJlLXJlbGVhc2UueW1sIn0sImdpdGh1Yl9oZWFkX3JlZiI6IiIsImdpdGh1Yl9yZWYiOiJyZWZzL2hlYWRzL2RldmVsb3AiLCJnaXRodWJfcmVmX3R5cGUiOiJicmFuY2giLCJnaXRodWJfcmVwb3NpdG9yeV9pZCI6IjIyMTkxOTM3OSIsImdpdGh1Yl9yZXBvc2l0b3J5X293bmVyIjoiYXdzLXBvd2VydG9vbHMiLCJnaXRodWJfcmVwb3NpdG9yeV9vd25lcl9pZCI6IjEyOTEyNzYzOCIsImdpdGh1Yl9ydW5fYXR0ZW1wdCI6IjEiLCJnaXRodWJfcnVuX2lkIjoiOTY1ODY3MzczMSIsImdpdGh1Yl9ydW5fbnVtYmVyIjoiNCIsImdpdGh1Yl9zaGExIjoiNjNmNWI1Y2M0NGM1Y2NhZTY2NTEzOTdjMmRlZTQ1MzJkY2IzNDZjZSJ9fSwibWV0YWRhdGEiOnsiYnVpbGRJbnZvY2F0aW9uSUQiOiI5NjU4NjczNzMxLTEiLCJjb21wbGV0ZW5lc3MiOnsicGFyYW1ldGVycyI6dHJ1ZSwiZW52aXJvbm1lbnQiOmZhbHNlLCJtYXRlcmlhbHMiOmZhbHNlfSwicmVwcm9kdWNpYmxlIjpmYWxzZX0sIm1hdGVyaWFscyI6W3sidXJpIjoiZ2l0K2h0dHBzOi8vZ2l0aHViLmNvbS9hd3MtcG93ZXJ0b29scy9wb3dlcnRvb2xzLWxhbWJkYS1weXRob25AcmVmcy9oZWFkcy9kZXZlbG9wIiwiZGlnZXN0Ijp7InNoYTEiOiI2M2Y1YjVjYzQ0YzVjY2FlNjY1MTM5N2MyZGVlNDUzMmRjYjM0NmNlIn19XX19","signatures":[{"keyid":"","sig":"MEYCIQD3YMttaa5pEA1Qc8T7/MpIOCwV3Ejt3YAVNqXePDzSUwIhAOnwcGPmD8cDN8wUHgyV0NS0A+H5xzKtUhdr9Id2bwLj","cert":"-----BEGIN CERTIFICATE-----\nMIIHZjCCBuygAwIBAgIUAK2jxqRvD0pf7pDD25n9DR1r9xUwCgYIKoZIzj0EAwMw\nNzEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MR4wHAYDVQQDExVzaWdzdG9yZS1pbnRl\ncm1lZGlhdGUwHhcNMjQwNjI1MDgwNzQwWhcNMjQwNjI1MDgxNzQwWjAAMFkwEwYH\nKoZIzj0CAQYIKoZIzj0DAQcDQgAEGeB59qg91wZ2BHLA6LPv69mOYNh/D2FD7H2f\n8GVXSwgTQPuOUFvy5VCL+CcsOuSG05/+jr339vcQEpRFUdpLfKOCBgswggYHMA4G\nA1UdDwEB/wQEAwIHgDATBgNVHSUEDDAKBggrBgEFBQcDAzAdBgNVHQ4EFgQUTkwO\nkfHvtDyt7ztc+8Hdw0p5OHwwHwYDVR0jBBgwFoAU39Ppz1YkEZb5qNjpKFWixi4Y\nZD8wgYQGA1UdEQEB/wR6MHiGdmh0dHBzOi8vZ2l0aHViLmNvbS9zbHNhLWZyYW1l\nd29yay9zbHNhLWdpdGh1Yi1nZW5lcmF0b3IvLmdpdGh1Yi93b3JrZmxvd3MvZ2Vu\nZXJhdG9yX2dlbmVyaWNfc2xzYTMueW1sQHJlZnMvdGFncy92Mi4wLjAwOQYKKwYB\nBAGDvzABAQQraHR0cHM6Ly90b2tlbi5hY3Rpb25zLmdpdGh1YnVzZXJjb250ZW50\nLmNvbTAWBgorBgEEAYO/MAECBAhzY2hlZHVsZTA2BgorBgEEAYO/MAEDBCg2M2Y1\nYjVjYzQ0YzVjY2FlNjY1MTM5N2MyZGVlNDUzMmRjYjM0NmNlMBkGCisGAQQBg78w\nAQQEC1ByZS1SZWxlYXNlMDUGCisGAQQBg78wAQUEJ2F3cy1wb3dlcnRvb2xzL3Bv\nd2VydG9vbHMtbGFtYmRhLXB5dGhvbjAgBgorBgEEAYO/MAEGBBJyZWZzL2hlYWRz\nL2RldmVsb3AwOwYKKwYBBAGDvzABCAQtDCtodHRwczovL3Rva2VuLmFjdGlvbnMu\nZ2l0aHVidXNlcmNvbnRlbnQuY29tMIGGBgorBgEEAYO/MAEJBHgMdmh0dHBzOi8v\nZ2l0aHViLmNvbS9zbHNhLWZyYW1ld29yay9zbHNhLWdpdGh1Yi1nZW5lcmF0b3Iv\nLmdpdGh1Yi93b3JrZmxvd3MvZ2VuZXJhdG9yX2dlbmVyaWNfc2xzYTMueW1sQHJl\nZnMvdGFncy92Mi4wLjAwOAYKKwYBBAGDvzABCgQqDCg1YTc3NWIzNjdhNTZkNWJk\nMTE4YTIyNGE4MTFiYmEyODgxNTBhNTYzMB0GCisGAQQBg78wAQsEDwwNZ2l0aHVi\nLWhvc3RlZDBKBgorBgEEAYO/MAEMBDwMOmh0dHBzOi8vZ2l0aHViLmNvbS9hd3Mt\ncG93ZXJ0b29scy9wb3dlcnRvb2xzLWxhbWJkYS1weXRob24wOAYKKwYBBAGDvzAB\nDQQqDCg2M2Y1YjVjYzQ0YzVjY2FlNjY1MTM5N2MyZGVlNDUzMmRjYjM0NmNlMCIG\nCisGAQQBg78wAQ4EFAwScmVmcy9oZWFkcy9kZXZlbG9wMBkGCisGAQQBg78wAQ8E\nCwwJMjIxOTE5Mzc5MDEGCisGAQQBg78wARAEIwwhaHR0cHM6Ly9naXRodWIuY29t\nL2F3cy1wb3dlcnRvb2xzMBkGCisGAQQBg78wAREECwwJMTI5MTI3NjM4MH8GCisG\nAQQBg78wARIEcQxvaHR0cHM6Ly9naXRodWIuY29tL2F3cy1wb3dlcnRvb2xzL3Bv\nd2VydG9vbHMtbGFtYmRhLXB5dGhvbi8uZ2l0aHViL3dvcmtmbG93cy9wcmUtcmVs\nZWFzZS55bWxAcmVmcy9oZWFkcy9kZXZlbG9wMDgGCisGAQQBg78wARMEKgwoNjNm\nNWI1Y2M0NGM1Y2NhZTY2NTEzOTdjMmRlZTQ1MzJkY2IzNDZjZTAYBgorBgEEAYO/\nMAEUBAoMCHNjaGVkdWxlMG0GCisGAQQBg78wARUEXwxdaHR0cHM6Ly9naXRodWIu\nY29tL2F3cy1wb3dlcnRvb2xzL3Bvd2VydG9vbHMtbGFtYmRhLXB5dGhvbi9hY3Rp\nb25zL3J1bnMvOTY1ODY3MzczMS9hdHRlbXB0cy8xMBYGCisGAQQBg78wARYECAwG\ncHVibGljMIGLBgorBgEEAdZ5AgQCBH0EewB5AHcA3T0wasbHETJjGR4cmWc3AqJK\nXrjePK3/h4pygC8p7o4AAAGQTm+tdQAABAMASDBGAiEA5Wp521wA7peGhX7ysdpF\nfqvDHrFNMlIGhc/ZCO5ksXkCIQCML9/qoFVVJtq+WzNqbJ4xrAu8K/Qw+YfJHrVu\nA0BTsTAKBggqhkjOPQQDAwNoADBlAjBvM7PuHhkpS2UfhHoC5ps8SwxlsN4Xxsl8\niVSjfhfCscChxudcOMIA1CGuzm9ISicCMQDtw1O+psY8KKD9IymQJ6dMFciLxiOv\nJ5Zah0EwyutDQ+kVJeMUZZSZEgFayR4fxtQ=\n-----END CERTIFICATE-----\n"}]}
\ No newline at end of file
diff --git a/provenance/2.39.2a4/multiple.intoto.jsonl b/provenance/2.39.2a4/multiple.intoto.jsonl
new file mode 100644
index 00000000000..fe41d297f43
--- /dev/null
+++ b/provenance/2.39.2a4/multiple.intoto.jsonl
@@ -0,0 +1 @@
+{"payloadType":"application/vnd.in-toto+json","payload":"eyJfdHlwZSI6Imh0dHBzOi8vaW4tdG90by5pby9TdGF0ZW1lbnQvdjAuMSIsInByZWRpY2F0ZVR5cGUiOiJodHRwczovL3Nsc2EuZGV2L3Byb3ZlbmFuY2UvdjAuMiIsInN1YmplY3QiOlt7Im5hbWUiOiIuL2F3c19sYW1iZGFfcG93ZXJ0b29scy0yLjM5LjJhNC1weTMtbm9uZS1hbnkud2hsIiwiZGlnZXN0Ijp7InNoYTI1NiI6IjI0YzM3YTc3OTY1MDEyYWI4MDk2OGFkNTgyODc4M2QyOGE2ZmQ2OGEzNzg2NDc0MzBmNDM0MjQxNjRkMjZjNDkifX0seyJuYW1lIjoiLi9hd3NfbGFtYmRhX3Bvd2VydG9vbHMtMi4zOS4yYTQudGFyLmd6IiwiZGlnZXN0Ijp7InNoYTI1NiI6ImY0ODcyZjZmMGEzNjQ1NWNhZmM5NTYzMzRiY2FmYjY5MTc2YjhhYzdmYjViNjY4ZTUzODFkN2MwZTUwNjYyZDkifX1dLCJwcmVkaWNhdGUiOnsiYnVpbGRlciI6eyJpZCI6Imh0dHBzOi8vZ2l0aHViLmNvbS9zbHNhLWZyYW1ld29yay9zbHNhLWdpdGh1Yi1nZW5lcmF0b3IvLmdpdGh1Yi93b3JrZmxvd3MvZ2VuZXJhdG9yX2dlbmVyaWNfc2xzYTMueW1sQHJlZnMvdGFncy92Mi4wLjAifSwiYnVpbGRUeXBlIjoiaHR0cHM6Ly9naXRodWIuY29tL3Nsc2EtZnJhbWV3b3JrL3Nsc2EtZ2l0aHViLWdlbmVyYXRvci9nZW5lcmljQHYxIiwiaW52b2NhdGlvbiI6eyJjb25maWdTb3VyY2UiOnsidXJpIjoiZ2l0K2h0dHBzOi8vZ2l0aHViLmNvbS9hd3MtcG93ZXJ0b29scy9wb3dlcnRvb2xzLWxhbWJkYS1weXRob25AcmVmcy9oZWFkcy9kZXZlbG9wIiwiZGlnZXN0Ijp7InNoYTEiOiJkMTRlMGJkN2FhODVmZTc3NWQ3Yjk0MTljM2E5ZDJhYWM5Yjk1Y2ZiIn0sImVudHJ5UG9pbnQiOiIuZ2l0aHViL3dvcmtmbG93cy9wcmUtcmVsZWFzZS55bWwifSwicGFyYW1ldGVycyI6e30sImVudmlyb25tZW50Ijp7ImdpdGh1Yl9hY3RvciI6ImxlYW5kcm9kYW1hc2NlbmEiLCJnaXRodWJfYWN0b3JfaWQiOiI0Mjk1MTczIiwiZ2l0aHViX2Jhc2VfcmVmIjoiIiwiZ2l0aHViX2V2ZW50X25hbWUiOiJzY2hlZHVsZSIsImdpdGh1Yl9ldmVudF9wYXlsb2FkIjp7ImVudGVycHJpc2UiOnsiYXZhdGFyX3VybCI6Imh0dHBzOi8vYXZhdGFycy5naXRodWJ1c2VyY29udGVudC5jb20vYi8xMjkwP3Y9NCIsImNyZWF0ZWRfYXQiOiIyMDE5LTExLTEzVDE4OjA1OjQxWiIsImRlc2NyaXB0aW9uIjoiIiwiaHRtbF91cmwiOiJodHRwczovL2dpdGh1Yi5jb20vZW50ZXJwcmlzZXMvYW1hem9uIiwiaWQiOjEyOTAsIm5hbWUiOiJBbWF6b24iLCJub2RlX2lkIjoiTURFd09rVnVkR1Z5Y0hKcGMyVXhNamt3Iiwic2x1ZyI6ImFtYXpvbiIsInVwZGF0ZWRfYXQiOiIyMDIzLTAxLTI3VDE0OjU2OjEwWiIsIndlYnNpdGVfdXJsIjoiaHR0cHM6Ly93d3cuYW1hem9uLmNvbS8ifSwib3JnYW5pemF0aW9uIjp7ImF2YXRhcl91cmwiOiJodHRwczovL2F2YXRhcnMuZ2l0aHVidXNlcmNvbnRlbnQuY29tL3UvMTI5MTI3NjM4P3Y9NCIsImRlc2NyaXB0aW9uIjoiIiwiZXZlbnRzX3VybCI6Imh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vb3Jncy9hd3MtcG93ZXJ0b29scy9ldmVudHMiLCJob29rc191cmwiOiJodHRwczovL2FwaS5naXRodWIuY29tL29yZ3MvYXdzLXBvd2VydG9vbHMvaG9va3MiLCJpZCI6MTI5MTI3NjM4LCJpc3N1ZXNfdXJsIjoiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9vcmdzL2F3cy1wb3dlcnRvb2xzL2lzc3VlcyIsImxvZ2luIjoiYXdzLXBvd2VydG9vbHMiLCJtZW1iZXJzX3VybCI6Imh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vb3Jncy9hd3MtcG93ZXJ0b29scy9tZW1iZXJzey9tZW1iZXJ9Iiwibm9kZV9pZCI6Ik9fa2dET0I3SlUxZyIsInB1YmxpY19tZW1iZXJzX3VybCI6Imh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vb3Jncy9hd3MtcG93ZXJ0b29scy9wdWJsaWNfbWVtYmVyc3svbWVtYmVyfSIsInJlcG9zX3VybCI6Imh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vb3Jncy9hd3MtcG93ZXJ0b29scy9yZXBvcyIsInVybCI6Imh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vb3Jncy9hd3MtcG93ZXJ0b29scyJ9LCJyZXBvc2l0b3J5Ijp7ImFsbG93X2ZvcmtpbmciOnRydWUsImFyY2hpdmVfdXJsIjoiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9hd3MtcG93ZXJ0b29scy9wb3dlcnRvb2xzLWxhbWJkYS1weXRob24ve2FyY2hpdmVfZm9ybWF0fXsvcmVmfSIsImFyY2hpdmVkIjpmYWxzZSwiYXNzaWduZWVzX3VybCI6Imh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYXdzLXBvd2VydG9vbHMvcG93ZXJ0b29scy1sYW1iZGEtcHl0aG9uL2Fzc2lnbmVlc3svdXNlcn0iLCJibG9ic191cmwiOiJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2F3cy1wb3dlcnRvb2xzL3Bvd2VydG9vbHMtbGFtYmRhLXB5dGhvbi9naXQvYmxvYnN7L3NoYX0iLCJicmFuY2hlc191cmwiOiJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2F3cy1wb3dlcnRvb2xzL3Bvd2VydG9vbHMtbGFtYmRhLXB5dGhvbi9icmFuY2hlc3svYnJhbmNofSIsImNsb25lX3VybCI6Imh0dHBzOi8vZ2l0aHViLmNvbS9hd3MtcG93ZXJ0b29scy9wb3dlcnRvb2xzLWxhbWJkYS1weXRob24uZ2l0IiwiY29sbGFib3JhdG9yc191cmwiOiJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2F3cy1wb3dlcnRvb2xzL3Bvd2VydG9vbHMtbGFtYmRhLXB5dGhvbi9jb2xsYWJvcmF0b3Jzey9jb2xsYWJvcmF0b3J9IiwiY29tbWVudHNfdXJsIjoiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9hd3MtcG93ZXJ0b29scy9wb3dlcnRvb2xzLWxhbWJkYS1weXRob24vY29tbWVudHN7L251bWJlcn0iLCJjb21taXRzX3VybCI6Imh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYXdzLXBvd2VydG9vbHMvcG93ZXJ0b29scy1sYW1iZGEtcHl0aG9uL2NvbW1pdHN7L3NoYX0iLCJjb21wYXJlX3VybCI6Imh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYXdzLXBvd2VydG9vbHMvcG93ZXJ0b29scy1sYW1iZGEtcHl0aG9uL2NvbXBhcmUve2Jhc2V9Li4ue2hlYWR9IiwiY29udGVudHNfdXJsIjoiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9hd3MtcG93ZXJ0b29scy9wb3dlcnRvb2xzLWxhbWJkYS1weXRob24vY29udGVudHMveytwYXRofSIsImNvbnRyaWJ1dG9yc191cmwiOiJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2F3cy1wb3dlcnRvb2xzL3Bvd2VydG9vbHMtbGFtYmRhLXB5dGhvbi9jb250cmlidXRvcnMiLCJjcmVhdGVkX2F0IjoiMjAxOS0xMS0xNVQxMjoyNjoxMloiLCJkZWZhdWx0X2JyYW5jaCI6ImRldmVsb3AiLCJkZXBsb3ltZW50c191cmwiOiJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2F3cy1wb3dlcnRvb2xzL3Bvd2VydG9vbHMtbGFtYmRhLXB5dGhvbi9kZXBsb3ltZW50cyIsImRlc2NyaXB0aW9uIjoiQSBkZXZlbG9wZXIgdG9vbGtpdCB0byBpbXBsZW1lbnQgU2VydmVybGVzcyBiZXN0IHByYWN0aWNlcyBhbmQgaW5jcmVhc2UgZGV2ZWxvcGVyIHZlbG9jaXR5LiIsImRpc2FibGVkIjpmYWxzZSwiZG93bmxvYWRzX3VybCI6Imh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYXdzLXBvd2VydG9vbHMvcG93ZXJ0b29scy1sYW1iZGEtcHl0aG9uL2Rvd25sb2FkcyIsImV2ZW50c191cmwiOiJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2F3cy1wb3dlcnRvb2xzL3Bvd2VydG9vbHMtbGFtYmRhLXB5dGhvbi9ldmVudHMiLCJmb3JrIjpmYWxzZSwiZm9ya3MiOjM3NiwiZm9ya3NfY291bnQiOjM3NiwiZm9ya3NfdXJsIjoiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9hd3MtcG93ZXJ0b29scy9wb3dlcnRvb2xzLWxhbWJkYS1weXRob24vZm9ya3MiLCJmdWxsX25hbWUiOiJhd3MtcG93ZXJ0b29scy9wb3dlcnRvb2xzLWxhbWJkYS1weXRob24iLCJnaXRfY29tbWl0c191cmwiOiJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2F3cy1wb3dlcnRvb2xzL3Bvd2VydG9vbHMtbGFtYmRhLXB5dGhvbi9naXQvY29tbWl0c3svc2hhfSIsImdpdF9yZWZzX3VybCI6Imh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYXdzLXBvd2VydG9vbHMvcG93ZXJ0b29scy1sYW1iZGEtcHl0aG9uL2dpdC9yZWZzey9zaGF9IiwiZ2l0X3RhZ3NfdXJsIjoiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9hd3MtcG93ZXJ0b29scy9wb3dlcnRvb2xzLWxhbWJkYS1weXRob24vZ2l0L3RhZ3N7L3NoYX0iLCJnaXRfdXJsIjoiZ2l0Oi8vZ2l0aHViLmNvbS9hd3MtcG93ZXJ0b29scy9wb3dlcnRvb2xzLWxhbWJkYS1weXRob24uZ2l0IiwiaGFzX2Rpc2N1c3Npb25zIjp0cnVlLCJoYXNfZG93bmxvYWRzIjp0cnVlLCJoYXNfaXNzdWVzIjp0cnVlLCJoYXNfcGFnZXMiOmZhbHNlLCJoYXNfcHJvamVjdHMiOnRydWUsImhhc193aWtpIjpmYWxzZSwiaG9tZXBhZ2UiOiJodHRwczovL2RvY3MucG93ZXJ0b29scy5hd3MuZGV2L2xhbWJkYS9weXRob24vbGF0ZXN0LyIsImhvb2tzX3VybCI6Imh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYXdzLXBvd2VydG9vbHMvcG93ZXJ0b29scy1sYW1iZGEtcHl0aG9uL2hvb2tzIiwiaHRtbF91cmwiOiJodHRwczovL2dpdGh1Yi5jb20vYXdzLXBvd2VydG9vbHMvcG93ZXJ0b29scy1sYW1iZGEtcHl0aG9uIiwiaWQiOjIyMTkxOTM3OSwiaXNfdGVtcGxhdGUiOmZhbHNlLCJpc3N1ZV9jb21tZW50X3VybCI6Imh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYXdzLXBvd2VydG9vbHMvcG93ZXJ0b29scy1sYW1iZGEtcHl0aG9uL2lzc3Vlcy9jb21tZW50c3svbnVtYmVyfSIsImlzc3VlX2V2ZW50c191cmwiOiJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2F3cy1wb3dlcnRvb2xzL3Bvd2VydG9vbHMtbGFtYmRhLXB5dGhvbi9pc3N1ZXMvZXZlbnRzey9udW1iZXJ9IiwiaXNzdWVzX3VybCI6Imh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYXdzLXBvd2VydG9vbHMvcG93ZXJ0b29scy1sYW1iZGEtcHl0aG9uL2lzc3Vlc3svbnVtYmVyfSIsImtleXNfdXJsIjoiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9hd3MtcG93ZXJ0b29scy9wb3dlcnRvb2xzLWxhbWJkYS1weXRob24va2V5c3sva2V5X2lkfSIsImxhYmVsc191cmwiOiJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2F3cy1wb3dlcnRvb2xzL3Bvd2VydG9vbHMtbGFtYmRhLXB5dGhvbi9sYWJlbHN7L25hbWV9IiwibGFuZ3VhZ2UiOiJQeXRob24iLCJsYW5ndWFnZXNfdXJsIjoiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9hd3MtcG93ZXJ0b29scy9wb3dlcnRvb2xzLWxhbWJkYS1weXRob24vbGFuZ3VhZ2VzIiwibGljZW5zZSI6eyJrZXkiOiJtaXQtMCIsIm5hbWUiOiJNSVQgTm8gQXR0cmlidXRpb24iLCJub2RlX2lkIjoiTURjNlRHbGpaVzV6WlRReCIsInNwZHhfaWQiOiJNSVQtMCIsInVybCI6Imh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vbGljZW5zZXMvbWl0LTAifSwibWVyZ2VzX3VybCI6Imh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYXdzLXBvd2VydG9vbHMvcG93ZXJ0b29scy1sYW1iZGEtcHl0aG9uL21lcmdlcyIsIm1pbGVzdG9uZXNfdXJsIjoiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9hd3MtcG93ZXJ0b29scy9wb3dlcnRvb2xzLWxhbWJkYS1weXRob24vbWlsZXN0b25lc3svbnVtYmVyfSIsIm1pcnJvcl91cmwiOm51bGwsIm5hbWUiOiJwb3dlcnRvb2xzLWxhbWJkYS1weXRob24iLCJub2RlX2lkIjoiTURFd09sSmxjRzl6YVhSdmNua3lNakU1TVRrek56az0iLCJub3RpZmljYXRpb25zX3VybCI6Imh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYXdzLXBvd2VydG9vbHMvcG93ZXJ0b29scy1sYW1iZGEtcHl0aG9uL25vdGlmaWNhdGlvbnN7P3NpbmNlLGFsbCxwYXJ0aWNpcGF0aW5nfSIsIm9wZW5faXNzdWVzIjoxMDAsIm9wZW5faXNzdWVzX2NvdW50IjoxMDAsIm93bmVyIjp7ImF2YXRhcl91cmwiOiJodHRwczovL2F2YXRhcnMuZ2l0aHVidXNlcmNvbnRlbnQuY29tL3UvMTI5MTI3NjM4P3Y9NCIsImV2ZW50c191cmwiOiJodHRwczovL2FwaS5naXRodWIuY29tL3VzZXJzL2F3cy1wb3dlcnRvb2xzL2V2ZW50c3svcHJpdmFjeX0iLCJmb2xsb3dlcnNfdXJsIjoiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS91c2Vycy9hd3MtcG93ZXJ0b29scy9mb2xsb3dlcnMiLCJmb2xsb3dpbmdfdXJsIjoiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS91c2Vycy9hd3MtcG93ZXJ0b29scy9mb2xsb3dpbmd7L290aGVyX3VzZXJ9IiwiZ2lzdHNfdXJsIjoiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS91c2Vycy9hd3MtcG93ZXJ0b29scy9naXN0c3svZ2lzdF9pZH0iLCJncmF2YXRhcl9pZCI6IiIsImh0bWxfdXJsIjoiaHR0cHM6Ly9naXRodWIuY29tL2F3cy1wb3dlcnRvb2xzIiwiaWQiOjEyOTEyNzYzOCwibG9naW4iOiJhd3MtcG93ZXJ0b29scyIsIm5vZGVfaWQiOiJPX2tnRE9CN0pVMWciLCJvcmdhbml6YXRpb25zX3VybCI6Imh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vdXNlcnMvYXdzLXBvd2VydG9vbHMvb3JncyIsInJlY2VpdmVkX2V2ZW50c191cmwiOiJodHRwczovL2FwaS5naXRodWIuY29tL3VzZXJzL2F3cy1wb3dlcnRvb2xzL3JlY2VpdmVkX2V2ZW50cyIsInJlcG9zX3VybCI6Imh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vdXNlcnMvYXdzLXBvd2VydG9vbHMvcmVwb3MiLCJzaXRlX2FkbWluIjpmYWxzZSwic3RhcnJlZF91cmwiOiJodHRwczovL2FwaS5naXRodWIuY29tL3VzZXJzL2F3cy1wb3dlcnRvb2xzL3N0YXJyZWR7L293bmVyfXsvcmVwb30iLCJzdWJzY3JpcHRpb25zX3VybCI6Imh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vdXNlcnMvYXdzLXBvd2VydG9vbHMvc3Vic2NyaXB0aW9ucyIsInR5cGUiOiJPcmdhbml6YXRpb24iLCJ1cmwiOiJodHRwczovL2FwaS5naXRodWIuY29tL3VzZXJzL2F3cy1wb3dlcnRvb2xzIn0sInByaXZhdGUiOmZhbHNlLCJwdWxsc191cmwiOiJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2F3cy1wb3dlcnRvb2xzL3Bvd2VydG9vbHMtbGFtYmRhLXB5dGhvbi9wdWxsc3svbnVtYmVyfSIsInB1c2hlZF9hdCI6IjIwMjQtMDYtMjZUMDc6NTI6MTBaIiwicmVsZWFzZXNfdXJsIjoiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9hd3MtcG93ZXJ0b29scy9wb3dlcnRvb2xzLWxhbWJkYS1weXRob24vcmVsZWFzZXN7L2lkfSIsInNpemUiOjQyNzk0LCJzc2hfdXJsIjoiZ2l0QGdpdGh1Yi5jb206YXdzLXBvd2VydG9vbHMvcG93ZXJ0b29scy1sYW1iZGEtcHl0aG9uLmdpdCIsInN0YXJnYXplcnNfY291bnQiOjI3MTIsInN0YXJnYXplcnNfdXJsIjoiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9hd3MtcG93ZXJ0b29scy9wb3dlcnRvb2xzLWxhbWJkYS1weXRob24vc3RhcmdhemVycyIsInN0YXR1c2VzX3VybCI6Imh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYXdzLXBvd2VydG9vbHMvcG93ZXJ0b29scy1sYW1iZGEtcHl0aG9uL3N0YXR1c2VzL3tzaGF9Iiwic3Vic2NyaWJlcnNfdXJsIjoiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9hd3MtcG93ZXJ0b29scy9wb3dlcnRvb2xzLWxhbWJkYS1weXRob24vc3Vic2NyaWJlcnMiLCJzdWJzY3JpcHRpb25fdXJsIjoiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9hd3MtcG93ZXJ0b29scy9wb3dlcnRvb2xzLWxhbWJkYS1weXRob24vc3Vic2NyaXB0aW9uIiwic3ZuX3VybCI6Imh0dHBzOi8vZ2l0aHViLmNvbS9hd3MtcG93ZXJ0b29scy9wb3dlcnRvb2xzLWxhbWJkYS1weXRob24iLCJ0YWdzX3VybCI6Imh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYXdzLXBvd2VydG9vbHMvcG93ZXJ0b29scy1sYW1iZGEtcHl0aG9uL3RhZ3MiLCJ0ZWFtc191cmwiOiJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2F3cy1wb3dlcnRvb2xzL3Bvd2VydG9vbHMtbGFtYmRhLXB5dGhvbi90ZWFtcyIsInRvcGljcyI6WyJhd3MiLCJhd3MtbGFtYmRhIiwiaGFja3RvYmVyZmVzdCIsImxhbWJkYSIsInB5dGhvbiIsInNlcnZlcmxlc3MiXSwidHJlZXNfdXJsIjoiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9hd3MtcG93ZXJ0b29scy9wb3dlcnRvb2xzLWxhbWJkYS1weXRob24vZ2l0L3RyZWVzey9zaGF9IiwidXBkYXRlZF9hdCI6IjIwMjQtMDYtMjZUMDg6MDE6MzlaIiwidXJsIjoiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9hd3MtcG93ZXJ0b29scy9wb3dlcnRvb2xzLWxhbWJkYS1weXRob24iLCJ2aXNpYmlsaXR5IjoicHVibGljIiwid2F0Y2hlcnMiOjI3MTIsIndhdGNoZXJzX2NvdW50IjoyNzEyLCJ3ZWJfY29tbWl0X3NpZ25vZmZfcmVxdWlyZWQiOnRydWV9LCJzY2hlZHVsZSI6IjAgOCAqICogMS01Iiwid29ya2Zsb3ciOiIuZ2l0aHViL3dvcmtmbG93cy9wcmUtcmVsZWFzZS55bWwifSwiZ2l0aHViX2hlYWRfcmVmIjoiIiwiZ2l0aHViX3JlZiI6InJlZnMvaGVhZHMvZGV2ZWxvcCIsImdpdGh1Yl9yZWZfdHlwZSI6ImJyYW5jaCIsImdpdGh1Yl9yZXBvc2l0b3J5X2lkIjoiMjIxOTE5Mzc5IiwiZ2l0aHViX3JlcG9zaXRvcnlfb3duZXIiOiJhd3MtcG93ZXJ0b29scyIsImdpdGh1Yl9yZXBvc2l0b3J5X293bmVyX2lkIjoiMTI5MTI3NjM4IiwiZ2l0aHViX3J1bl9hdHRlbXB0IjoiMSIsImdpdGh1Yl9ydW5faWQiOiI5Njc1ODE0ODAxIiwiZ2l0aHViX3J1bl9udW1iZXIiOiI1IiwiZ2l0aHViX3NoYTEiOiJkMTRlMGJkN2FhODVmZTc3NWQ3Yjk0MTljM2E5ZDJhYWM5Yjk1Y2ZiIn19LCJtZXRhZGF0YSI6eyJidWlsZEludm9jYXRpb25JRCI6Ijk2NzU4MTQ4MDEtMSIsImNvbXBsZXRlbmVzcyI6eyJwYXJhbWV0ZXJzIjp0cnVlLCJlbnZpcm9ubWVudCI6ZmFsc2UsIm1hdGVyaWFscyI6ZmFsc2V9LCJyZXByb2R1Y2libGUiOmZhbHNlfSwibWF0ZXJpYWxzIjpbeyJ1cmkiOiJnaXQraHR0cHM6Ly9naXRodWIuY29tL2F3cy1wb3dlcnRvb2xzL3Bvd2VydG9vbHMtbGFtYmRhLXB5dGhvbkByZWZzL2hlYWRzL2RldmVsb3AiLCJkaWdlc3QiOnsic2hhMSI6ImQxNGUwYmQ3YWE4NWZlNzc1ZDdiOTQxOWMzYTlkMmFhYzliOTVjZmIifX1dfX0=","signatures":[{"keyid":"","sig":"MEQCICwmcOzobfi/tmLVa8XA9bznXQg7UFpD/vRwIRz0kMO6AiASDJ7ROXqOpbrfW/776osg8I4xJZkIJA1+heEUyrE+4A==","cert":"-----BEGIN CERTIFICATE-----\nMIIHYzCCBuqgAwIBAgIUB5SYtoG1cBkAo1KUPqdSSYuMXgwwCgYIKoZIzj0EAwMw\nNzEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MR4wHAYDVQQDExVzaWdzdG9yZS1pbnRl\ncm1lZGlhdGUwHhcNMjQwNjI2MDgwNzUwWhcNMjQwNjI2MDgxNzUwWjAAMFkwEwYH\nKoZIzj0CAQYIKoZIzj0DAQcDQgAE5jNg9cSC7PzQU5xdwb/Xza1THGLUgL0A39je\nGaIcLZk/amOY0daQsDZXXJuZWMAjktKlt2QXbtQmi6YYdscM2aOCBgkwggYFMA4G\nA1UdDwEB/wQEAwIHgDATBgNVHSUEDDAKBggrBgEFBQcDAzAdBgNVHQ4EFgQU7uyp\nNLsKP0UK8kT5KNgx+MCCG7gwHwYDVR0jBBgwFoAU39Ppz1YkEZb5qNjpKFWixi4Y\nZD8wgYQGA1UdEQEB/wR6MHiGdmh0dHBzOi8vZ2l0aHViLmNvbS9zbHNhLWZyYW1l\nd29yay9zbHNhLWdpdGh1Yi1nZW5lcmF0b3IvLmdpdGh1Yi93b3JrZmxvd3MvZ2Vu\nZXJhdG9yX2dlbmVyaWNfc2xzYTMueW1sQHJlZnMvdGFncy92Mi4wLjAwOQYKKwYB\nBAGDvzABAQQraHR0cHM6Ly90b2tlbi5hY3Rpb25zLmdpdGh1YnVzZXJjb250ZW50\nLmNvbTAWBgorBgEEAYO/MAECBAhzY2hlZHVsZTA2BgorBgEEAYO/MAEDBChkMTRl\nMGJkN2FhODVmZTc3NWQ3Yjk0MTljM2E5ZDJhYWM5Yjk1Y2ZiMBkGCisGAQQBg78w\nAQQEC1ByZS1SZWxlYXNlMDUGCisGAQQBg78wAQUEJ2F3cy1wb3dlcnRvb2xzL3Bv\nd2VydG9vbHMtbGFtYmRhLXB5dGhvbjAgBgorBgEEAYO/MAEGBBJyZWZzL2hlYWRz\nL2RldmVsb3AwOwYKKwYBBAGDvzABCAQtDCtodHRwczovL3Rva2VuLmFjdGlvbnMu\nZ2l0aHVidXNlcmNvbnRlbnQuY29tMIGGBgorBgEEAYO/MAEJBHgMdmh0dHBzOi8v\nZ2l0aHViLmNvbS9zbHNhLWZyYW1ld29yay9zbHNhLWdpdGh1Yi1nZW5lcmF0b3Iv\nLmdpdGh1Yi93b3JrZmxvd3MvZ2VuZXJhdG9yX2dlbmVyaWNfc2xzYTMueW1sQHJl\nZnMvdGFncy92Mi4wLjAwOAYKKwYBBAGDvzABCgQqDCg1YTc3NWIzNjdhNTZkNWJk\nMTE4YTIyNGE4MTFiYmEyODgxNTBhNTYzMB0GCisGAQQBg78wAQsEDwwNZ2l0aHVi\nLWhvc3RlZDBKBgorBgEEAYO/MAEMBDwMOmh0dHBzOi8vZ2l0aHViLmNvbS9hd3Mt\ncG93ZXJ0b29scy9wb3dlcnRvb2xzLWxhbWJkYS1weXRob24wOAYKKwYBBAGDvzAB\nDQQqDChkMTRlMGJkN2FhODVmZTc3NWQ3Yjk0MTljM2E5ZDJhYWM5Yjk1Y2ZiMCIG\nCisGAQQBg78wAQ4EFAwScmVmcy9oZWFkcy9kZXZlbG9wMBkGCisGAQQBg78wAQ8E\nCwwJMjIxOTE5Mzc5MDEGCisGAQQBg78wARAEIwwhaHR0cHM6Ly9naXRodWIuY29t\nL2F3cy1wb3dlcnRvb2xzMBkGCisGAQQBg78wAREECwwJMTI5MTI3NjM4MH8GCisG\nAQQBg78wARIEcQxvaHR0cHM6Ly9naXRodWIuY29tL2F3cy1wb3dlcnRvb2xzL3Bv\nd2VydG9vbHMtbGFtYmRhLXB5dGhvbi8uZ2l0aHViL3dvcmtmbG93cy9wcmUtcmVs\nZWFzZS55bWxAcmVmcy9oZWFkcy9kZXZlbG9wMDgGCisGAQQBg78wARMEKgwoZDE0\nZTBiZDdhYTg1ZmU3NzVkN2I5NDE5YzNhOWQyYWFjOWI5NWNmYjAYBgorBgEEAYO/\nMAEUBAoMCHNjaGVkdWxlMG0GCisGAQQBg78wARUEXwxdaHR0cHM6Ly9naXRodWIu\nY29tL2F3cy1wb3dlcnRvb2xzL3Bvd2VydG9vbHMtbGFtYmRhLXB5dGhvbi9hY3Rp\nb25zL3J1bnMvOTY3NTgxNDgwMS9hdHRlbXB0cy8xMBYGCisGAQQBg78wARYECAwG\ncHVibGljMIGJBgorBgEEAdZ5AgQCBHsEeQB3AHUA3T0wasbHETJjGR4cmWc3AqJK\nXrjePK3/h4pygC8p7o4AAAGQU5YzDgAABAMARjBEAiAOkUSMDbgl1XvsybIkd4Qi\nLbPRJd1CjsqciwHUiS9dMwIgdRHpeGmBiUd+mZjnofb4Z5Evx4ON8WHyOzUW67qx\nJe8wCgYIKoZIzj0EAwMDZwAwZAIwLjrV6X2YqTsdojJGNFHilmnioaTtA3/wN0lA\nI07ZlNZOKXaez74Tb5w1bhXRnIwAAjB/Z7PA4t4I5e3vYMRLEDK1CYrj4tpjdslA\n3CpsbDC3AqU0J01SoipEBafZuFTlB9E=\n-----END CERTIFICATE-----\n"}]}
\ No newline at end of file
diff --git a/provenance/2.39.2a5/multiple.intoto.jsonl b/provenance/2.39.2a5/multiple.intoto.jsonl
new file mode 100644
index 00000000000..6a13ed17f4e
--- /dev/null
+++ b/provenance/2.39.2a5/multiple.intoto.jsonl
@@ -0,0 +1 @@
+{"payloadType":"application/vnd.in-toto+json","payload":"eyJfdHlwZSI6Imh0dHBzOi8vaW4tdG90by5pby9TdGF0ZW1lbnQvdjAuMSIsInByZWRpY2F0ZVR5cGUiOiJodHRwczovL3Nsc2EuZGV2L3Byb3ZlbmFuY2UvdjAuMiIsInN1YmplY3QiOlt7Im5hbWUiOiIuL2F3c19sYW1iZGFfcG93ZXJ0b29scy0yLjM5LjJhNS1weTMtbm9uZS1hbnkud2hsIiwiZGlnZXN0Ijp7InNoYTI1NiI6ImEyZjE3OTVjOTBiMTJmZTMzZWQ5ZDQ1YzczNzRlOGIwYWFjNzU2MDc0MTNkNDk2YmY5OTEyOTJiY2I4MDFjZGIifX0seyJuYW1lIjoiLi9hd3NfbGFtYmRhX3Bvd2VydG9vbHMtMi4zOS4yYTUudGFyLmd6IiwiZGlnZXN0Ijp7InNoYTI1NiI6IjE3YTM2YjY3ZTQ4ZDRlYzcxZmMzMjgwODI3ZTIxZTc2M2NiOTI5NTcyNWJjNWVkZGM1YzE3OTkzOTQ0ZDc0MTUifX1dLCJwcmVkaWNhdGUiOnsiYnVpbGRlciI6eyJpZCI6Imh0dHBzOi8vZ2l0aHViLmNvbS9zbHNhLWZyYW1ld29yay9zbHNhLWdpdGh1Yi1nZW5lcmF0b3IvLmdpdGh1Yi93b3JrZmxvd3MvZ2VuZXJhdG9yX2dlbmVyaWNfc2xzYTMueW1sQHJlZnMvdGFncy92Mi4wLjAifSwiYnVpbGRUeXBlIjoiaHR0cHM6Ly9naXRodWIuY29tL3Nsc2EtZnJhbWV3b3JrL3Nsc2EtZ2l0aHViLWdlbmVyYXRvci9nZW5lcmljQHYxIiwiaW52b2NhdGlvbiI6eyJjb25maWdTb3VyY2UiOnsidXJpIjoiZ2l0K2h0dHBzOi8vZ2l0aHViLmNvbS9hd3MtcG93ZXJ0b29scy9wb3dlcnRvb2xzLWxhbWJkYS1weXRob25AcmVmcy9oZWFkcy9kZXZlbG9wIiwiZGlnZXN0Ijp7InNoYTEiOiI4NDY5OGYyNTJlODk4YTNlMzkyZTZhMTI2OGNlODlkZGFkMzBkZDMxIn0sImVudHJ5UG9pbnQiOiIuZ2l0aHViL3dvcmtmbG93cy9wcmUtcmVsZWFzZS55bWwifSwicGFyYW1ldGVycyI6e30sImVudmlyb25tZW50Ijp7ImdpdGh1Yl9hY3RvciI6ImxlYW5kcm9kYW1hc2NlbmEiLCJnaXRodWJfYWN0b3JfaWQiOiI0Mjk1MTczIiwiZ2l0aHViX2Jhc2VfcmVmIjoiIiwiZ2l0aHViX2V2ZW50X25hbWUiOiJzY2hlZHVsZSIsImdpdGh1Yl9ldmVudF9wYXlsb2FkIjp7ImVudGVycHJpc2UiOnsiYXZhdGFyX3VybCI6Imh0dHBzOi8vYXZhdGFycy5naXRodWJ1c2VyY29udGVudC5jb20vYi8xMjkwP3Y9NCIsImNyZWF0ZWRfYXQiOiIyMDE5LTExLTEzVDE4OjA1OjQxWiIsImRlc2NyaXB0aW9uIjoiIiwiaHRtbF91cmwiOiJodHRwczovL2dpdGh1Yi5jb20vZW50ZXJwcmlzZXMvYW1hem9uIiwiaWQiOjEyOTAsIm5hbWUiOiJBbWF6b24iLCJub2RlX2lkIjoiTURFd09rVnVkR1Z5Y0hKcGMyVXhNamt3Iiwic2x1ZyI6ImFtYXpvbiIsInVwZGF0ZWRfYXQiOiIyMDIzLTAxLTI3VDE0OjU2OjEwWiIsIndlYnNpdGVfdXJsIjoiaHR0cHM6Ly93d3cuYW1hem9uLmNvbS8ifSwib3JnYW5pemF0aW9uIjp7ImF2YXRhcl91cmwiOiJodHRwczovL2F2YXRhcnMuZ2l0aHVidXNlcmNvbnRlbnQuY29tL3UvMTI5MTI3NjM4P3Y9NCIsImRlc2NyaXB0aW9uIjoiIiwiZXZlbnRzX3VybCI6Imh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vb3Jncy9hd3MtcG93ZXJ0b29scy9ldmVudHMiLCJob29rc191cmwiOiJodHRwczovL2FwaS5naXRodWIuY29tL29yZ3MvYXdzLXBvd2VydG9vbHMvaG9va3MiLCJpZCI6MTI5MTI3NjM4LCJpc3N1ZXNfdXJsIjoiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9vcmdzL2F3cy1wb3dlcnRvb2xzL2lzc3VlcyIsImxvZ2luIjoiYXdzLXBvd2VydG9vbHMiLCJtZW1iZXJzX3VybCI6Imh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vb3Jncy9hd3MtcG93ZXJ0b29scy9tZW1iZXJzey9tZW1iZXJ9Iiwibm9kZV9pZCI6Ik9fa2dET0I3SlUxZyIsInB1YmxpY19tZW1iZXJzX3VybCI6Imh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vb3Jncy9hd3MtcG93ZXJ0b29scy9wdWJsaWNfbWVtYmVyc3svbWVtYmVyfSIsInJlcG9zX3VybCI6Imh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vb3Jncy9hd3MtcG93ZXJ0b29scy9yZXBvcyIsInVybCI6Imh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vb3Jncy9hd3MtcG93ZXJ0b29scyJ9LCJyZXBvc2l0b3J5Ijp7ImFsbG93X2ZvcmtpbmciOnRydWUsImFyY2hpdmVfdXJsIjoiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9hd3MtcG93ZXJ0b29scy9wb3dlcnRvb2xzLWxhbWJkYS1weXRob24ve2FyY2hpdmVfZm9ybWF0fXsvcmVmfSIsImFyY2hpdmVkIjpmYWxzZSwiYXNzaWduZWVzX3VybCI6Imh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYXdzLXBvd2VydG9vbHMvcG93ZXJ0b29scy1sYW1iZGEtcHl0aG9uL2Fzc2lnbmVlc3svdXNlcn0iLCJibG9ic191cmwiOiJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2F3cy1wb3dlcnRvb2xzL3Bvd2VydG9vbHMtbGFtYmRhLXB5dGhvbi9naXQvYmxvYnN7L3NoYX0iLCJicmFuY2hlc191cmwiOiJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2F3cy1wb3dlcnRvb2xzL3Bvd2VydG9vbHMtbGFtYmRhLXB5dGhvbi9icmFuY2hlc3svYnJhbmNofSIsImNsb25lX3VybCI6Imh0dHBzOi8vZ2l0aHViLmNvbS9hd3MtcG93ZXJ0b29scy9wb3dlcnRvb2xzLWxhbWJkYS1weXRob24uZ2l0IiwiY29sbGFib3JhdG9yc191cmwiOiJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2F3cy1wb3dlcnRvb2xzL3Bvd2VydG9vbHMtbGFtYmRhLXB5dGhvbi9jb2xsYWJvcmF0b3Jzey9jb2xsYWJvcmF0b3J9IiwiY29tbWVudHNfdXJsIjoiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9hd3MtcG93ZXJ0b29scy9wb3dlcnRvb2xzLWxhbWJkYS1weXRob24vY29tbWVudHN7L251bWJlcn0iLCJjb21taXRzX3VybCI6Imh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYXdzLXBvd2VydG9vbHMvcG93ZXJ0b29scy1sYW1iZGEtcHl0aG9uL2NvbW1pdHN7L3NoYX0iLCJjb21wYXJlX3VybCI6Imh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYXdzLXBvd2VydG9vbHMvcG93ZXJ0b29scy1sYW1iZGEtcHl0aG9uL2NvbXBhcmUve2Jhc2V9Li4ue2hlYWR9IiwiY29udGVudHNfdXJsIjoiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9hd3MtcG93ZXJ0b29scy9wb3dlcnRvb2xzLWxhbWJkYS1weXRob24vY29udGVudHMveytwYXRofSIsImNvbnRyaWJ1dG9yc191cmwiOiJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2F3cy1wb3dlcnRvb2xzL3Bvd2VydG9vbHMtbGFtYmRhLXB5dGhvbi9jb250cmlidXRvcnMiLCJjcmVhdGVkX2F0IjoiMjAxOS0xMS0xNVQxMjoyNjoxMloiLCJkZWZhdWx0X2JyYW5jaCI6ImRldmVsb3AiLCJkZXBsb3ltZW50c191cmwiOiJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2F3cy1wb3dlcnRvb2xzL3Bvd2VydG9vbHMtbGFtYmRhLXB5dGhvbi9kZXBsb3ltZW50cyIsImRlc2NyaXB0aW9uIjoiQSBkZXZlbG9wZXIgdG9vbGtpdCB0byBpbXBsZW1lbnQgU2VydmVybGVzcyBiZXN0IHByYWN0aWNlcyBhbmQgaW5jcmVhc2UgZGV2ZWxvcGVyIHZlbG9jaXR5LiIsImRpc2FibGVkIjpmYWxzZSwiZG93bmxvYWRzX3VybCI6Imh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYXdzLXBvd2VydG9vbHMvcG93ZXJ0b29scy1sYW1iZGEtcHl0aG9uL2Rvd25sb2FkcyIsImV2ZW50c191cmwiOiJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2F3cy1wb3dlcnRvb2xzL3Bvd2VydG9vbHMtbGFtYmRhLXB5dGhvbi9ldmVudHMiLCJmb3JrIjpmYWxzZSwiZm9ya3MiOjM3NiwiZm9ya3NfY291bnQiOjM3NiwiZm9ya3NfdXJsIjoiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9hd3MtcG93ZXJ0b29scy9wb3dlcnRvb2xzLWxhbWJkYS1weXRob24vZm9ya3MiLCJmdWxsX25hbWUiOiJhd3MtcG93ZXJ0b29scy9wb3dlcnRvb2xzLWxhbWJkYS1weXRob24iLCJnaXRfY29tbWl0c191cmwiOiJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2F3cy1wb3dlcnRvb2xzL3Bvd2VydG9vbHMtbGFtYmRhLXB5dGhvbi9naXQvY29tbWl0c3svc2hhfSIsImdpdF9yZWZzX3VybCI6Imh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYXdzLXBvd2VydG9vbHMvcG93ZXJ0b29scy1sYW1iZGEtcHl0aG9uL2dpdC9yZWZzey9zaGF9IiwiZ2l0X3RhZ3NfdXJsIjoiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9hd3MtcG93ZXJ0b29scy9wb3dlcnRvb2xzLWxhbWJkYS1weXRob24vZ2l0L3RhZ3N7L3NoYX0iLCJnaXRfdXJsIjoiZ2l0Oi8vZ2l0aHViLmNvbS9hd3MtcG93ZXJ0b29scy9wb3dlcnRvb2xzLWxhbWJkYS1weXRob24uZ2l0IiwiaGFzX2Rpc2N1c3Npb25zIjp0cnVlLCJoYXNfZG93bmxvYWRzIjp0cnVlLCJoYXNfaXNzdWVzIjp0cnVlLCJoYXNfcGFnZXMiOmZhbHNlLCJoYXNfcHJvamVjdHMiOnRydWUsImhhc193aWtpIjpmYWxzZSwiaG9tZXBhZ2UiOiJodHRwczovL2RvY3MucG93ZXJ0b29scy5hd3MuZGV2L2xhbWJkYS9weXRob24vbGF0ZXN0LyIsImhvb2tzX3VybCI6Imh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYXdzLXBvd2VydG9vbHMvcG93ZXJ0b29scy1sYW1iZGEtcHl0aG9uL2hvb2tzIiwiaHRtbF91cmwiOiJodHRwczovL2dpdGh1Yi5jb20vYXdzLXBvd2VydG9vbHMvcG93ZXJ0b29scy1sYW1iZGEtcHl0aG9uIiwiaWQiOjIyMTkxOTM3OSwiaXNfdGVtcGxhdGUiOmZhbHNlLCJpc3N1ZV9jb21tZW50X3VybCI6Imh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYXdzLXBvd2VydG9vbHMvcG93ZXJ0b29scy1sYW1iZGEtcHl0aG9uL2lzc3Vlcy9jb21tZW50c3svbnVtYmVyfSIsImlzc3VlX2V2ZW50c191cmwiOiJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2F3cy1wb3dlcnRvb2xzL3Bvd2VydG9vbHMtbGFtYmRhLXB5dGhvbi9pc3N1ZXMvZXZlbnRzey9udW1iZXJ9IiwiaXNzdWVzX3VybCI6Imh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYXdzLXBvd2VydG9vbHMvcG93ZXJ0b29scy1sYW1iZGEtcHl0aG9uL2lzc3Vlc3svbnVtYmVyfSIsImtleXNfdXJsIjoiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9hd3MtcG93ZXJ0b29scy9wb3dlcnRvb2xzLWxhbWJkYS1weXRob24va2V5c3sva2V5X2lkfSIsImxhYmVsc191cmwiOiJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2F3cy1wb3dlcnRvb2xzL3Bvd2VydG9vbHMtbGFtYmRhLXB5dGhvbi9sYWJlbHN7L25hbWV9IiwibGFuZ3VhZ2UiOiJQeXRob24iLCJsYW5ndWFnZXNfdXJsIjoiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9hd3MtcG93ZXJ0b29scy9wb3dlcnRvb2xzLWxhbWJkYS1weXRob24vbGFuZ3VhZ2VzIiwibGljZW5zZSI6eyJrZXkiOiJtaXQtMCIsIm5hbWUiOiJNSVQgTm8gQXR0cmlidXRpb24iLCJub2RlX2lkIjoiTURjNlRHbGpaVzV6WlRReCIsInNwZHhfaWQiOiJNSVQtMCIsInVybCI6Imh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vbGljZW5zZXMvbWl0LTAifSwibWVyZ2VzX3VybCI6Imh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYXdzLXBvd2VydG9vbHMvcG93ZXJ0b29scy1sYW1iZGEtcHl0aG9uL21lcmdlcyIsIm1pbGVzdG9uZXNfdXJsIjoiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9hd3MtcG93ZXJ0b29scy9wb3dlcnRvb2xzLWxhbWJkYS1weXRob24vbWlsZXN0b25lc3svbnVtYmVyfSIsIm1pcnJvcl91cmwiOm51bGwsIm5hbWUiOiJwb3dlcnRvb2xzLWxhbWJkYS1weXRob24iLCJub2RlX2lkIjoiTURFd09sSmxjRzl6YVhSdmNua3lNakU1TVRrek56az0iLCJub3RpZmljYXRpb25zX3VybCI6Imh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYXdzLXBvd2VydG9vbHMvcG93ZXJ0b29scy1sYW1iZGEtcHl0aG9uL25vdGlmaWNhdGlvbnN7P3NpbmNlLGFsbCxwYXJ0aWNpcGF0aW5nfSIsIm9wZW5faXNzdWVzIjo5NSwib3Blbl9pc3N1ZXNfY291bnQiOjk1LCJvd25lciI6eyJhdmF0YXJfdXJsIjoiaHR0cHM6Ly9hdmF0YXJzLmdpdGh1YnVzZXJjb250ZW50LmNvbS91LzEyOTEyNzYzOD92PTQiLCJldmVudHNfdXJsIjoiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS91c2Vycy9hd3MtcG93ZXJ0b29scy9ldmVudHN7L3ByaXZhY3l9IiwiZm9sbG93ZXJzX3VybCI6Imh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vdXNlcnMvYXdzLXBvd2VydG9vbHMvZm9sbG93ZXJzIiwiZm9sbG93aW5nX3VybCI6Imh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vdXNlcnMvYXdzLXBvd2VydG9vbHMvZm9sbG93aW5ney9vdGhlcl91c2VyfSIsImdpc3RzX3VybCI6Imh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vdXNlcnMvYXdzLXBvd2VydG9vbHMvZ2lzdHN7L2dpc3RfaWR9IiwiZ3JhdmF0YXJfaWQiOiIiLCJodG1sX3VybCI6Imh0dHBzOi8vZ2l0aHViLmNvbS9hd3MtcG93ZXJ0b29scyIsImlkIjoxMjkxMjc2MzgsImxvZ2luIjoiYXdzLXBvd2VydG9vbHMiLCJub2RlX2lkIjoiT19rZ0RPQjdKVTFnIiwib3JnYW5pemF0aW9uc191cmwiOiJodHRwczovL2FwaS5naXRodWIuY29tL3VzZXJzL2F3cy1wb3dlcnRvb2xzL29yZ3MiLCJyZWNlaXZlZF9ldmVudHNfdXJsIjoiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS91c2Vycy9hd3MtcG93ZXJ0b29scy9yZWNlaXZlZF9ldmVudHMiLCJyZXBvc191cmwiOiJodHRwczovL2FwaS5naXRodWIuY29tL3VzZXJzL2F3cy1wb3dlcnRvb2xzL3JlcG9zIiwic2l0ZV9hZG1pbiI6ZmFsc2UsInN0YXJyZWRfdXJsIjoiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS91c2Vycy9hd3MtcG93ZXJ0b29scy9zdGFycmVkey9vd25lcn17L3JlcG99Iiwic3Vic2NyaXB0aW9uc191cmwiOiJodHRwczovL2FwaS5naXRodWIuY29tL3VzZXJzL2F3cy1wb3dlcnRvb2xzL3N1YnNjcmlwdGlvbnMiLCJ0eXBlIjoiT3JnYW5pemF0aW9uIiwidXJsIjoiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS91c2Vycy9hd3MtcG93ZXJ0b29scyJ9LCJwcml2YXRlIjpmYWxzZSwicHVsbHNfdXJsIjoiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9hd3MtcG93ZXJ0b29scy9wb3dlcnRvb2xzLWxhbWJkYS1weXRob24vcHVsbHN7L251bWJlcn0iLCJwdXNoZWRfYXQiOiIyMDI0LTA2LTI3VDAwOjMxOjIwWiIsInJlbGVhc2VzX3VybCI6Imh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYXdzLXBvd2VydG9vbHMvcG93ZXJ0b29scy1sYW1iZGEtcHl0aG9uL3JlbGVhc2Vzey9pZH0iLCJzaXplIjo0MzI5MCwic3NoX3VybCI6ImdpdEBnaXRodWIuY29tOmF3cy1wb3dlcnRvb2xzL3Bvd2VydG9vbHMtbGFtYmRhLXB5dGhvbi5naXQiLCJzdGFyZ2F6ZXJzX2NvdW50IjoyNzEzLCJzdGFyZ2F6ZXJzX3VybCI6Imh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYXdzLXBvd2VydG9vbHMvcG93ZXJ0b29scy1sYW1iZGEtcHl0aG9uL3N0YXJnYXplcnMiLCJzdGF0dXNlc191cmwiOiJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2F3cy1wb3dlcnRvb2xzL3Bvd2VydG9vbHMtbGFtYmRhLXB5dGhvbi9zdGF0dXNlcy97c2hhfSIsInN1YnNjcmliZXJzX3VybCI6Imh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYXdzLXBvd2VydG9vbHMvcG93ZXJ0b29scy1sYW1iZGEtcHl0aG9uL3N1YnNjcmliZXJzIiwic3Vic2NyaXB0aW9uX3VybCI6Imh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYXdzLXBvd2VydG9vbHMvcG93ZXJ0b29scy1sYW1iZGEtcHl0aG9uL3N1YnNjcmlwdGlvbiIsInN2bl91cmwiOiJodHRwczovL2dpdGh1Yi5jb20vYXdzLXBvd2VydG9vbHMvcG93ZXJ0b29scy1sYW1iZGEtcHl0aG9uIiwidGFnc191cmwiOiJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2F3cy1wb3dlcnRvb2xzL3Bvd2VydG9vbHMtbGFtYmRhLXB5dGhvbi90YWdzIiwidGVhbXNfdXJsIjoiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9hd3MtcG93ZXJ0b29scy9wb3dlcnRvb2xzLWxhbWJkYS1weXRob24vdGVhbXMiLCJ0b3BpY3MiOlsiYXdzIiwiYXdzLWxhbWJkYSIsImhhY2t0b2JlcmZlc3QiLCJsYW1iZGEiLCJweXRob24iLCJzZXJ2ZXJsZXNzIl0sInRyZWVzX3VybCI6Imh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYXdzLXBvd2VydG9vbHMvcG93ZXJ0b29scy1sYW1iZGEtcHl0aG9uL2dpdC90cmVlc3svc2hhfSIsInVwZGF0ZWRfYXQiOiIyMDI0LTA2LTI2VDIzOjQ4OjUzWiIsInVybCI6Imh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYXdzLXBvd2VydG9vbHMvcG93ZXJ0b29scy1sYW1iZGEtcHl0aG9uIiwidmlzaWJpbGl0eSI6InB1YmxpYyIsIndhdGNoZXJzIjoyNzEzLCJ3YXRjaGVyc19jb3VudCI6MjcxMywid2ViX2NvbW1pdF9zaWdub2ZmX3JlcXVpcmVkIjp0cnVlfSwic2NoZWR1bGUiOiIwIDggKiAqIDEtNSIsIndvcmtmbG93IjoiLmdpdGh1Yi93b3JrZmxvd3MvcHJlLXJlbGVhc2UueW1sIn0sImdpdGh1Yl9oZWFkX3JlZiI6IiIsImdpdGh1Yl9yZWYiOiJyZWZzL2hlYWRzL2RldmVsb3AiLCJnaXRodWJfcmVmX3R5cGUiOiJicmFuY2giLCJnaXRodWJfcmVwb3NpdG9yeV9pZCI6IjIyMTkxOTM3OSIsImdpdGh1Yl9yZXBvc2l0b3J5X293bmVyIjoiYXdzLXBvd2VydG9vbHMiLCJnaXRodWJfcmVwb3NpdG9yeV9vd25lcl9pZCI6IjEyOTEyNzYzOCIsImdpdGh1Yl9ydW5fYXR0ZW1wdCI6IjEiLCJnaXRodWJfcnVuX2lkIjoiOTY5Mjg3MjEzOCIsImdpdGh1Yl9ydW5fbnVtYmVyIjoiNiIsImdpdGh1Yl9zaGExIjoiODQ2OThmMjUyZTg5OGEzZTM5MmU2YTEyNjhjZTg5ZGRhZDMwZGQzMSJ9fSwibWV0YWRhdGEiOnsiYnVpbGRJbnZvY2F0aW9uSUQiOiI5NjkyODcyMTM4LTEiLCJjb21wbGV0ZW5lc3MiOnsicGFyYW1ldGVycyI6dHJ1ZSwiZW52aXJvbm1lbnQiOmZhbHNlLCJtYXRlcmlhbHMiOmZhbHNlfSwicmVwcm9kdWNpYmxlIjpmYWxzZX0sIm1hdGVyaWFscyI6W3sidXJpIjoiZ2l0K2h0dHBzOi8vZ2l0aHViLmNvbS9hd3MtcG93ZXJ0b29scy9wb3dlcnRvb2xzLWxhbWJkYS1weXRob25AcmVmcy9oZWFkcy9kZXZlbG9wIiwiZGlnZXN0Ijp7InNoYTEiOiI4NDY5OGYyNTJlODk4YTNlMzkyZTZhMTI2OGNlODlkZGFkMzBkZDMxIn19XX19","signatures":[{"keyid":"","sig":"MEUCIEyoVJUjmhvxjI8hApH62a+he8gs91BWS/09mm5Zw9qjAiEAlXw7CYCQfdYPrRHYpxIZnEXpDxQAS5FvK/gVlsU1XIU=","cert":"-----BEGIN CERTIFICATE-----\nMIIHZDCCBuugAwIBAgIURDCWTFVw0njJuo7sBCPoGPmeaiswCgYIKoZIzj0EAwMw\nNzEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MR4wHAYDVQQDExVzaWdzdG9yZS1pbnRl\ncm1lZGlhdGUwHhcNMjQwNjI3MDgwNzQyWhcNMjQwNjI3MDgxNzQyWjAAMFkwEwYH\nKoZIzj0CAQYIKoZIzj0DAQcDQgAE0l5fi1hLGgXF+JILCZ7zqMLPufzl9IlgkcF2\n9N6iITx7KGo7ZK+RuKuxVwrWdm1oeu3R80IPr/IYk18MRYfe4KOCBgowggYGMA4G\nA1UdDwEB/wQEAwIHgDATBgNVHSUEDDAKBggrBgEFBQcDAzAdBgNVHQ4EFgQUImCE\n4j0HPzaOEZr2y8r57SCLgfQwHwYDVR0jBBgwFoAU39Ppz1YkEZb5qNjpKFWixi4Y\nZD8wgYQGA1UdEQEB/wR6MHiGdmh0dHBzOi8vZ2l0aHViLmNvbS9zbHNhLWZyYW1l\nd29yay9zbHNhLWdpdGh1Yi1nZW5lcmF0b3IvLmdpdGh1Yi93b3JrZmxvd3MvZ2Vu\nZXJhdG9yX2dlbmVyaWNfc2xzYTMueW1sQHJlZnMvdGFncy92Mi4wLjAwOQYKKwYB\nBAGDvzABAQQraHR0cHM6Ly90b2tlbi5hY3Rpb25zLmdpdGh1YnVzZXJjb250ZW50\nLmNvbTAWBgorBgEEAYO/MAECBAhzY2hlZHVsZTA2BgorBgEEAYO/MAEDBCg4NDY5\nOGYyNTJlODk4YTNlMzkyZTZhMTI2OGNlODlkZGFkMzBkZDMxMBkGCisGAQQBg78w\nAQQEC1ByZS1SZWxlYXNlMDUGCisGAQQBg78wAQUEJ2F3cy1wb3dlcnRvb2xzL3Bv\nd2VydG9vbHMtbGFtYmRhLXB5dGhvbjAgBgorBgEEAYO/MAEGBBJyZWZzL2hlYWRz\nL2RldmVsb3AwOwYKKwYBBAGDvzABCAQtDCtodHRwczovL3Rva2VuLmFjdGlvbnMu\nZ2l0aHVidXNlcmNvbnRlbnQuY29tMIGGBgorBgEEAYO/MAEJBHgMdmh0dHBzOi8v\nZ2l0aHViLmNvbS9zbHNhLWZyYW1ld29yay9zbHNhLWdpdGh1Yi1nZW5lcmF0b3Iv\nLmdpdGh1Yi93b3JrZmxvd3MvZ2VuZXJhdG9yX2dlbmVyaWNfc2xzYTMueW1sQHJl\nZnMvdGFncy92Mi4wLjAwOAYKKwYBBAGDvzABCgQqDCg1YTc3NWIzNjdhNTZkNWJk\nMTE4YTIyNGE4MTFiYmEyODgxNTBhNTYzMB0GCisGAQQBg78wAQsEDwwNZ2l0aHVi\nLWhvc3RlZDBKBgorBgEEAYO/MAEMBDwMOmh0dHBzOi8vZ2l0aHViLmNvbS9hd3Mt\ncG93ZXJ0b29scy9wb3dlcnRvb2xzLWxhbWJkYS1weXRob24wOAYKKwYBBAGDvzAB\nDQQqDCg4NDY5OGYyNTJlODk4YTNlMzkyZTZhMTI2OGNlODlkZGFkMzBkZDMxMCIG\nCisGAQQBg78wAQ4EFAwScmVmcy9oZWFkcy9kZXZlbG9wMBkGCisGAQQBg78wAQ8E\nCwwJMjIxOTE5Mzc5MDEGCisGAQQBg78wARAEIwwhaHR0cHM6Ly9naXRodWIuY29t\nL2F3cy1wb3dlcnRvb2xzMBkGCisGAQQBg78wAREECwwJMTI5MTI3NjM4MH8GCisG\nAQQBg78wARIEcQxvaHR0cHM6Ly9naXRodWIuY29tL2F3cy1wb3dlcnRvb2xzL3Bv\nd2VydG9vbHMtbGFtYmRhLXB5dGhvbi8uZ2l0aHViL3dvcmtmbG93cy9wcmUtcmVs\nZWFzZS55bWxAcmVmcy9oZWFkcy9kZXZlbG9wMDgGCisGAQQBg78wARMEKgwoODQ2\nOThmMjUyZTg5OGEzZTM5MmU2YTEyNjhjZTg5ZGRhZDMwZGQzMTAYBgorBgEEAYO/\nMAEUBAoMCHNjaGVkdWxlMG0GCisGAQQBg78wARUEXwxdaHR0cHM6Ly9naXRodWIu\nY29tL2F3cy1wb3dlcnRvb2xzL3Bvd2VydG9vbHMtbGFtYmRhLXB5dGhvbi9hY3Rp\nb25zL3J1bnMvOTY5Mjg3MjEzOC9hdHRlbXB0cy8xMBYGCisGAQQBg78wARYECAwG\ncHVibGljMIGKBgorBgEEAdZ5AgQCBHwEegB4AHYA3T0wasbHETJjGR4cmWc3AqJK\nXrjePK3/h4pygC8p7o4AAAGQWLxvdwAABAMARzBFAiAyEO61rMo/vxCViebjwHfG\nbyhEOZWH52lJEPwVyPeyJwIhANCPMDQ6C7D79/vkFKs3NKMBbx63BQ53DpNUzrBF\nvzQhMAoGCCqGSM49BAMDA2cAMGQCMFT9Yl89vnQQ3jyCEY80amZfgo2o92GNexh5\nWmMj9Fc/0rziTmbzxXHrX4OhgRXfbQIwVHw+DkyOwUDYtNnjwUJxOD8Z/z/ghOiJ\nJ+BJJqQBIfVmZHpScglFDED9Z5Sde24N\n-----END CERTIFICATE-----\n"}]}
\ No newline at end of file
diff --git a/pyproject.toml b/pyproject.toml
index 49eb3285936..2973632c43f 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[tool.poetry]
name = "aws_lambda_powertools"
-version = "2.39.0"
+version = "2.40.0"
description = "Powertools for AWS Lambda (Python) is a developer toolkit to implement Serverless best practices and increase developer velocity."
authors = ["Amazon Web Services"]
include = ["aws_lambda_powertools/py.typed", "THIRD-PARTY-LICENSES"]
@@ -62,40 +62,40 @@ boto3 = "^1.26.164"
isort = "^5.13.2"
pytest-cov = "^5.0.0"
pytest-mock = "^3.14.0"
-pdoc3 = "^0.10.0"
+pdoc3 = "^0.11.0"
pytest-asyncio = "^0.23.7"
-bandit = "^1.7.8"
+bandit = "^1.7.9"
radon = "^6.0.1"
xenon = "^0.9.1"
mkdocs-git-revision-date-plugin = "^0.3.2"
-mike = "^2.1.1"
+mike = "^2.1.2"
pytest-xdist = "^3.6.1"
aws-cdk-lib = "^2.145.0"
"aws-cdk.aws-apigatewayv2-alpha" = "^2.38.1-alpha.0"
"aws-cdk.aws-apigatewayv2-integrations-alpha" = "^2.38.1-alpha.0"
"aws-cdk.aws-apigatewayv2-authorizers-alpha" = "^2.38.1-alpha.0"
"aws-cdk.aws-lambda-python-alpha" = "^2.145.0a0"
-"cdklabs.generative-ai-cdk-constructs" = "^0.1.179"
+"cdklabs.generative-ai-cdk-constructs" = "^0.1.198"
pytest-benchmark = "^4.0.0"
mypy-boto3-appconfig = "^1.34.58"
mypy-boto3-cloudformation = "^1.34.111"
mypy-boto3-cloudwatch = "^1.34.83"
-mypy-boto3-dynamodb = "^1.34.114"
+mypy-boto3-dynamodb = "^1.34.131"
mypy-boto3-lambda = "^1.34.77"
mypy-boto3-logs = "^1.34.66"
-mypy-boto3-secretsmanager = "^1.34.109"
-mypy-boto3-ssm = "^1.34.91"
+mypy-boto3-secretsmanager = "^1.34.128"
+mypy-boto3-ssm = "^1.34.132"
mypy-boto3-s3 = "^1.34.120"
mypy-boto3-xray = "^1.34.0"
types-requests = "^2.31.0"
typing-extensions = "^4.12.2"
-mkdocs-material = "^9.5.26"
-filelock = "^3.14.0"
+mkdocs-material = "^9.5.27"
+filelock = "^3.15.4"
checksumdir = "^1.2.0"
mypy-boto3-appconfigdata = "^1.34.24"
ijson = "^3.3.0"
typed-ast = { version = "^1.5.5", python = "< 3.8" }
-hvac = "^2.2.0"
+hvac = "^2.3.0"
aws-requests-auth = "^0.4.3"
datadog-lambda = "^6.96.0"
@@ -115,17 +115,19 @@ datadog = ["datadog-lambda"]
datamasking = ["aws-encryption-sdk", "jsonpath-ng"]
[tool.poetry.group.dev.dependencies]
-cfn-lint = "0.87.6"
+cfn-lint = "1.3.7"
mypy = "^1.1.1"
types-python-dateutil = "^2.8.19.6"
+aws-cdk-aws-appsync-alpha = "^2.59.0a0"
httpx = ">=0.23.3,<0.28.0"
sentry-sdk = ">=1.22.2,<3.0.0"
-ruff = ">=0.0.272,<0.4.9"
+ruff = ">=0.0.272,<0.4.11"
retry2 = "^0.9.5"
pytest-socket = ">=0.6,<0.8"
types-redis = "^4.6.0.7"
testcontainers = { extras = ["redis"], version = "^3.7.1" }
multiprocess = "^0.70.16"
+nox = "^2024.4.15"
[tool.coverage.run]
source = ["aws_lambda_powertools"]
diff --git a/tests/functional/validator/__init__.py b/tests/__init__.py
similarity index 100%
rename from tests/functional/validator/__init__.py
rename to tests/__init__.py
diff --git a/tests/e2e/event_handler_appsync/__init__.py b/tests/e2e/event_handler_appsync/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/tests/e2e/event_handler_appsync/conftest.py b/tests/e2e/event_handler_appsync/conftest.py
new file mode 100644
index 00000000000..1f6d8c406de
--- /dev/null
+++ b/tests/e2e/event_handler_appsync/conftest.py
@@ -0,0 +1,19 @@
+import pytest
+
+from tests.e2e.event_handler_appsync.infrastructure import EventHandlerAppSyncStack
+
+
+@pytest.fixture(autouse=True, scope="package")
+def infrastructure():
+ """Setup and teardown logic for E2E test infrastructure
+
+ Yields
+ ------
+ Dict[str, str]
+ CloudFormation Outputs from deployed infrastructure
+ """
+ stack = EventHandlerAppSyncStack()
+ try:
+ yield stack.deploy()
+ finally:
+ stack.delete()
diff --git a/tests/e2e/event_handler_appsync/files/schema.graphql b/tests/e2e/event_handler_appsync/files/schema.graphql
new file mode 100644
index 00000000000..9733ba2f666
--- /dev/null
+++ b/tests/e2e/event_handler_appsync/files/schema.graphql
@@ -0,0 +1,22 @@
+schema {
+ query: Query
+}
+
+type Query {
+ getPost(post_id:ID!): Post
+ allPosts: [Post]
+}
+
+type Post {
+ post_id: ID!
+ author: String!
+ title: String
+ content: String
+ url: String
+ ups: Int
+ downs: Int
+ relatedPosts: [Post]
+ relatedPostsAsync: [Post]
+ relatedPostsAggregate: [Post]
+ relatedPostsAsyncAggregate: [Post]
+}
diff --git a/tests/e2e/event_handler_appsync/handlers/appsync_resolver_handler.py b/tests/e2e/event_handler_appsync/handlers/appsync_resolver_handler.py
new file mode 100644
index 00000000000..594290f478d
--- /dev/null
+++ b/tests/e2e/event_handler_appsync/handlers/appsync_resolver_handler.py
@@ -0,0 +1,114 @@
+from typing import List, Optional
+
+from pydantic import BaseModel
+
+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()
+
+
+posts = {
+ "1": {
+ "post_id": "1",
+ "title": "First book",
+ "author": "Author1",
+ "url": "https://amazon.com/",
+ "content": "SAMPLE TEXT AUTHOR 1",
+ "ups": "100",
+ "downs": "10",
+ },
+ "2": {
+ "post_id": "2",
+ "title": "Second book",
+ "author": "Author2",
+ "url": "https://amazon.com",
+ "content": "SAMPLE TEXT AUTHOR 2",
+ "ups": "100",
+ "downs": "10",
+ },
+ "3": {
+ "post_id": "3",
+ "title": "Third book",
+ "author": "Author3",
+ "url": None,
+ "content": None,
+ "ups": None,
+ "downs": None,
+ },
+ "4": {
+ "post_id": "4",
+ "title": "Fourth book",
+ "author": "Author4",
+ "url": "https://www.amazon.com/",
+ "content": "SAMPLE TEXT AUTHOR 4",
+ "ups": "1000",
+ "downs": "0",
+ },
+ "5": {
+ "post_id": "5",
+ "title": "Fifth book",
+ "author": "Author5",
+ "url": "https://www.amazon.com/",
+ "content": "SAMPLE TEXT AUTHOR 5",
+ "ups": "50",
+ "downs": "0",
+ },
+}
+
+posts_related = {
+ "1": [posts["4"]],
+ "2": [posts["3"], posts["5"]],
+ "3": [posts["2"], posts["1"]],
+ "4": [posts["2"], posts["1"]],
+ "5": [],
+}
+
+
+class Post(BaseModel):
+ post_id: str
+ author: str
+ title: str
+ url: str
+ content: str
+ ups: str
+ downs: str
+
+
+# PROCESSING SINGLE RESOLVERS
+@app.resolver(type_name="Query", field_name="getPost")
+def get_post(post_id: str = "") -> dict:
+ post = Post(**posts[post_id]).dict()
+ return post
+
+
+@app.resolver(type_name="Query", field_name="allPosts")
+def all_posts() -> List[dict]:
+ return list(posts.values())
+
+
+# PROCESSING BATCH WITHOUT AGGREGATION
+@app.batch_resolver(type_name="Post", field_name="relatedPosts", aggregate=False)
+def related_posts(event: AppSyncResolverEvent) -> Optional[list]:
+ return posts_related[event.source["post_id"]] if event.source else None
+
+
+@app.async_batch_resolver(type_name="Post", field_name="relatedPostsAsync", aggregate=False)
+async def related_posts_async(event: AppSyncResolverEvent) -> Optional[list]:
+ return posts_related[event.source["post_id"]] if event.source else None
+
+
+# PROCESSING BATCH WITH AGGREGATION
+@app.batch_resolver(type_name="Post", field_name="relatedPostsAggregate")
+def related_posts_aggregate(event: List[AppSyncResolverEvent]) -> Optional[list]:
+ return [posts_related[record.source.get("post_id")] for record in event]
+
+
+@app.async_batch_resolver(type_name="Post", field_name="relatedPostsAsyncAggregate")
+async def related_posts_async_aggregate(event: List[AppSyncResolverEvent]) -> Optional[list]:
+ return [posts_related[record.source.get("post_id")] for record in event]
+
+
+def lambda_handler(event, context: LambdaContext) -> dict:
+ return app.resolve(event, context)
diff --git a/tests/e2e/event_handler_appsync/infrastructure.py b/tests/e2e/event_handler_appsync/infrastructure.py
new file mode 100644
index 00000000000..1a07270572a
--- /dev/null
+++ b/tests/e2e/event_handler_appsync/infrastructure.py
@@ -0,0 +1,76 @@
+from pathlib import Path
+
+from aws_cdk import CfnOutput, Duration, Expiration
+from aws_cdk import aws_appsync_alpha as appsync
+from aws_cdk.aws_lambda import Function
+
+from tests.e2e.utils.data_builder import build_random_value
+from tests.e2e.utils.infrastructure import BaseInfrastructure
+
+
+class EventHandlerAppSyncStack(BaseInfrastructure):
+ def create_resources(self):
+ functions = self.create_lambda_functions()
+
+ self._create_appsync_endpoint(function=functions["AppsyncResolverHandler"])
+
+ def _create_appsync_endpoint(self, function: Function):
+ api = appsync.GraphqlApi(
+ self.stack,
+ "Api",
+ name=f"e2e-tests{build_random_value()}",
+ schema=appsync.SchemaFile.from_asset(str(Path(self.feature_path, "files/schema.graphql"))),
+ authorization_config=appsync.AuthorizationConfig(
+ default_authorization=appsync.AuthorizationMode(
+ authorization_type=appsync.AuthorizationType.API_KEY,
+ api_key_config=appsync.ApiKeyConfig(
+ description="public key for getting data",
+ expires=Expiration.after(Duration.hours(25)),
+ name="API Token",
+ ),
+ ),
+ ),
+ xray_enabled=False,
+ )
+ lambda_datasource = api.add_lambda_data_source("DataSource", lambda_function=function)
+
+ lambda_datasource.create_resolver(
+ "QueryGetAllPostsResolver",
+ type_name="Query",
+ field_name="allPosts",
+ )
+ lambda_datasource.create_resolver(
+ "QueryGetPostResolver",
+ type_name="Query",
+ field_name="getPost",
+ )
+ lambda_datasource.create_resolver(
+ "QueryGetPostRelatedResolver",
+ type_name="Post",
+ field_name="relatedPosts",
+ max_batch_size=10,
+ )
+
+ lambda_datasource.create_resolver(
+ "QueryGetPostRelatedAsyncResolver",
+ type_name="Post",
+ field_name="relatedPostsAsync",
+ max_batch_size=10,
+ )
+
+ lambda_datasource.create_resolver(
+ "QueryGetPostRelatedResolverAggregate",
+ type_name="Post",
+ field_name="relatedPostsAggregate",
+ max_batch_size=10,
+ )
+
+ lambda_datasource.create_resolver(
+ "QueryGetPostRelatedAsyncResolverAggregate",
+ type_name="Post",
+ field_name="relatedPostsAsyncAggregate",
+ max_batch_size=10,
+ )
+
+ CfnOutput(self.stack, "GraphQLHTTPUrl", value=api.graphql_url)
+ CfnOutput(self.stack, "GraphQLAPIKey", value=api.api_key)
diff --git a/tests/e2e/event_handler_appsync/test_appsync_resolvers.py b/tests/e2e/event_handler_appsync/test_appsync_resolvers.py
new file mode 100644
index 00000000000..35549a1fdef
--- /dev/null
+++ b/tests/e2e/event_handler_appsync/test_appsync_resolvers.py
@@ -0,0 +1,176 @@
+import json
+
+import pytest
+from requests import Request
+
+from tests.e2e.utils import data_fetcher
+
+
+@pytest.fixture
+def appsync_endpoint(infrastructure: dict) -> str:
+ return infrastructure["GraphQLHTTPUrl"]
+
+
+@pytest.fixture
+def appsync_access_key(infrastructure: dict) -> str:
+ return infrastructure["GraphQLAPIKey"]
+
+
+@pytest.mark.xdist_group(name="event_handler")
+def test_appsync_get_all_posts(appsync_endpoint, appsync_access_key):
+ # GIVEN
+ body = {
+ "query": "query MyQuery { allPosts { post_id }}",
+ "variables": None,
+ "operationName": "MyQuery",
+ }
+
+ # WHEN
+ response = data_fetcher.get_http_response(
+ Request(
+ method="POST",
+ url=appsync_endpoint,
+ json=body,
+ headers={"x-api-key": appsync_access_key, "Content-Type": "application/json"},
+ ),
+ )
+
+ # THEN expect a HTTP 200 response and content return list of Posts
+ assert response.status_code == 200
+ assert response.content is not None
+
+ data = json.loads(response.content.decode("ascii"))["data"]
+
+ assert data["allPosts"] is not None
+ assert len(data["allPosts"]) > 0
+
+
+@pytest.mark.xdist_group(name="event_handler")
+def test_appsync_get_post(appsync_endpoint, appsync_access_key):
+ # GIVEN
+ post_id = "1"
+ body = {
+ "query": f'query MyQuery {{ getPost(post_id: "{post_id}") {{ post_id }} }}',
+ "variables": None,
+ "operationName": "MyQuery",
+ }
+
+ # WHEN
+ response = data_fetcher.get_http_response(
+ Request(
+ method="POST",
+ url=appsync_endpoint,
+ json=body,
+ headers={"x-api-key": appsync_access_key, "Content-Type": "application/json"},
+ ),
+ )
+
+ # THEN expect a HTTP 200 response and content return Post id
+ assert response.status_code == 200
+ assert response.content is not None
+
+ data = json.loads(response.content.decode("ascii"))["data"]
+
+ assert data["getPost"]["post_id"] == post_id
+
+
+@pytest.mark.xdist_group(name="event_handler")
+def test_appsync_get_related_posts_batch_without_aggregate(appsync_endpoint, appsync_access_key):
+ # GIVEN a batch event
+ post_id = "2"
+ related_posts_ids = ["3", "5"]
+
+ body = {
+ "query": f"""
+ query MyQuery {{
+ getPost(post_id: "{post_id}") {{
+ post_id
+ relatedPosts {{
+ post_id
+ }}
+ relatedPostsAsync {{
+ post_id
+ }}
+ }}
+ }}
+ """,
+ "variables": None,
+ "operationName": "MyQuery",
+ }
+
+ # WHEN we invoke the AppSync API with a batch event
+ response = data_fetcher.get_http_response(
+ Request(
+ method="POST",
+ url=appsync_endpoint,
+ json=body,
+ headers={"x-api-key": appsync_access_key, "Content-Type": "application/json"},
+ ),
+ )
+
+ # THEN expect a HTTP 200 response and content return Post id with dependent Posts id's
+ assert response.status_code == 200
+ assert response.content is not None
+
+ data = json.loads(response.content.decode("ascii"))["data"]
+
+ assert data["getPost"]["post_id"] == post_id
+
+ assert len(data["getPost"]["relatedPosts"]) == len(related_posts_ids)
+ for post in data["getPost"]["relatedPosts"]:
+ assert post["post_id"] in related_posts_ids
+
+ assert len(data["getPost"]["relatedPostsAsync"]) == len(related_posts_ids)
+ for post in data["getPost"]["relatedPostsAsync"]:
+ assert post["post_id"] in related_posts_ids
+
+
+@pytest.mark.xdist_group(name="event_handler")
+def test_appsync_get_related_posts_batch_with_aggregate(appsync_endpoint, appsync_access_key):
+ # GIVEN a batch event
+ post_id = "2"
+ related_posts_ids = ["3", "5"]
+
+ body = {
+ "query": f"""
+ query MyQuery {{
+ getPost(post_id: "{post_id}") {{
+ post_id
+ relatedPostsAggregate {{
+ post_id
+ }}
+ relatedPostsAsyncAggregate {{
+ post_id
+ }}
+ }}
+ }}
+ """,
+ "variables": None,
+ "operationName": "MyQuery",
+ }
+
+ # WHEN we invoke the AppSync API with a batch event
+ response = data_fetcher.get_http_response(
+ Request(
+ method="POST",
+ url=appsync_endpoint,
+ json=body,
+ headers={"x-api-key": appsync_access_key, "Content-Type": "application/json"},
+ ),
+ )
+
+ # THEN expect a HTTP 200 response and content return Post id with dependent Posts id's
+ assert response.status_code == 200
+ assert response.content is not None
+
+ data = json.loads(response.content.decode("ascii"))["data"]
+
+ assert data["getPost"]["post_id"] == post_id
+
+ assert len(data["getPost"]["relatedPostsAggregate"]) == len(related_posts_ids)
+ for post in data["getPost"]["relatedPostsAggregate"]:
+ assert post["post_id"] in related_posts_ids
+
+ assert len(data["getPost"]["relatedPostsAsyncAggregate"]) == len(related_posts_ids)
+ for post in data["getPost"]["relatedPostsAsyncAggregate"]:
+ assert post["post_id"] in related_posts_ids
diff --git a/tests/functional/batch/_pydantic/__init__.py b/tests/functional/batch/_pydantic/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/tests/functional/batch/sample_models.py b/tests/functional/batch/_pydantic/sample_models.py
similarity index 100%
rename from tests/functional/batch/sample_models.py
rename to tests/functional/batch/_pydantic/sample_models.py
diff --git a/tests/functional/batch/_pydantic/test_utilities_batch_pydantic.py b/tests/functional/batch/_pydantic/test_utilities_batch_pydantic.py
new file mode 100644
index 00000000000..fa2babec2d8
--- /dev/null
+++ b/tests/functional/batch/_pydantic/test_utilities_batch_pydantic.py
@@ -0,0 +1,641 @@
+import json
+import uuid
+from random import randint
+from typing import Any, Awaitable, Callable, Dict, Optional
+
+import pytest
+
+from aws_lambda_powertools.utilities.batch import (
+ AsyncBatchProcessor,
+ BatchProcessor,
+ EventType,
+ SqsFifoPartialProcessor,
+ batch_processor,
+)
+from aws_lambda_powertools.utilities.data_classes.dynamo_db_stream_event import (
+ DynamoDBRecord,
+)
+from aws_lambda_powertools.utilities.data_classes.kinesis_stream_event import (
+ KinesisStreamRecord,
+)
+from aws_lambda_powertools.utilities.data_classes.sqs_event import SQSRecord
+from aws_lambda_powertools.utilities.parser import BaseModel, validator
+from aws_lambda_powertools.utilities.parser.models import (
+ DynamoDBStreamChangedRecordModel,
+ DynamoDBStreamRecordModel,
+ SqsRecordModel,
+)
+from aws_lambda_powertools.utilities.parser.types import Literal
+from tests.functional.batch._pydantic.sample_models import (
+ OrderDynamoDBRecord,
+ OrderKinesisRecord,
+ OrderSqs,
+)
+from tests.functional.utils import b64_to_str, str_to_b64
+
+
+@pytest.fixture(scope="module")
+def sqs_event_fifo_factory() -> Callable:
+ def factory(body: str, message_group_id: str = ""):
+ return {
+ "messageId": f"{uuid.uuid4()}",
+ "receiptHandle": "AQEBwJnKyrHigUMZj6rYigCgxlaS3SLy0a",
+ "body": body,
+ "attributes": {
+ "ApproximateReceiveCount": "1",
+ "SentTimestamp": "1703675223472",
+ "SequenceNumber": "18882884930918384133",
+ "MessageGroupId": message_group_id,
+ "SenderId": "SenderId",
+ "MessageDeduplicationId": "1eea03c3f7e782c7bdc2f2a917f40389314733ff39f5ab16219580c0109ade98",
+ "ApproximateFirstReceiveTimestamp": "1703675223484",
+ },
+ "messageAttributes": {},
+ "md5OfBody": "e4e68fb7bd0e697a0ae8f1bb342846b3",
+ "eventSource": "aws:sqs",
+ "eventSourceARN": "arn:aws:sqs:us-east-2:123456789012:my-queue",
+ "awsRegion": "us-east-1",
+ }
+
+ return factory
+
+
+@pytest.fixture(scope="module")
+def sqs_event_factory() -> Callable:
+ def factory(body: str):
+ return {
+ "messageId": f"{uuid.uuid4()}",
+ "receiptHandle": "AQEBwJnKyrHigUMZj6rYigCgxlaS3SLy0a",
+ "body": body,
+ "attributes": {
+ "ApproximateReceiveCount": "1",
+ "SentTimestamp": "1545082649183",
+ "SenderId": "SenderId",
+ "ApproximateFirstReceiveTimestamp": "1545082649185",
+ },
+ "messageAttributes": {},
+ "md5OfBody": "e4e68fb7bd0e697a0ae8f1bb342846b3",
+ "eventSource": "aws:sqs",
+ "eventSourceARN": "arn:aws:sqs:us-east-2:123456789012:my-queue",
+ "awsRegion": "us-east-1",
+ }
+
+ return factory
+
+
+@pytest.fixture(scope="module")
+def kinesis_event_factory() -> Callable:
+ def factory(body: str):
+ seq = "".join(str(randint(0, 9)) for _ in range(52))
+ return {
+ "kinesis": {
+ "kinesisSchemaVersion": "1.0",
+ "partitionKey": "1",
+ "sequenceNumber": seq,
+ "data": str_to_b64(body),
+ "approximateArrivalTimestamp": 1545084650.987,
+ },
+ "eventSource": "aws:kinesis",
+ "eventVersion": "1.0",
+ "eventID": f"shardId-000000000006:{seq}",
+ "eventName": "aws:kinesis:record",
+ "invokeIdentityArn": "arn:aws:iam::123456789012:role/lambda-role",
+ "awsRegion": "us-east-2",
+ "eventSourceARN": "arn:aws:kinesis:us-east-2:123456789012:stream/lambda-stream",
+ }
+
+ return factory
+
+
+@pytest.fixture(scope="module")
+def dynamodb_event_factory() -> Callable:
+ def factory(body: str):
+ seq = "".join(str(randint(0, 9)) for _ in range(10))
+ return {
+ "eventID": "1",
+ "eventVersion": "1.0",
+ "dynamodb": {
+ "Keys": {"Id": {"N": "101"}},
+ "NewImage": {"Message": {"S": body}},
+ "StreamViewType": "NEW_AND_OLD_IMAGES",
+ "SequenceNumber": seq,
+ "SizeBytes": 26,
+ },
+ "awsRegion": "us-west-2",
+ "eventName": "INSERT",
+ "eventSourceARN": "eventsource_arn",
+ "eventSource": "aws:dynamodb",
+ }
+
+ return factory
+
+
+@pytest.fixture(scope="module")
+def record_handler() -> Callable:
+ def handler(record):
+ body = record["body"]
+ if "fail" in body:
+ raise Exception("Failed to process record.")
+ return body
+
+ return handler
+
+
+@pytest.fixture(scope="module")
+def record_handler_model() -> Callable:
+ def record_handler(record: OrderSqs):
+ if "fail" in record.body.item["type"]:
+ raise Exception("Failed to process record.")
+ return record.body.item
+
+ return record_handler
+
+
+@pytest.fixture(scope="module")
+def async_record_handler() -> Callable[..., Awaitable[Any]]:
+ async def handler(record):
+ body = record["body"]
+ if "fail" in body:
+ raise Exception("Failed to process record.")
+ return body
+
+ return handler
+
+
+@pytest.fixture(scope="module")
+def async_record_handler_model() -> Callable[..., Awaitable[Any]]:
+ async def async_record_handler(record: OrderSqs):
+ if "fail" in record.body.item["type"]:
+ raise ValueError("Failed to process record.")
+ return record.body.item
+
+ return async_record_handler
+
+
+@pytest.fixture(scope="module")
+def kinesis_record_handler() -> Callable:
+ def handler(record: KinesisStreamRecord):
+ body = b64_to_str(record.kinesis.data)
+ if "fail" in body:
+ raise Exception("Failed to process record.")
+ return body
+
+ return handler
+
+
+@pytest.fixture(scope="module")
+def kinesis_record_handler_model() -> Callable:
+ def record_handler(record: OrderKinesisRecord):
+ if "fail" in record.kinesis.data.item["type"]:
+ raise ValueError("Failed to process record.")
+ return record.kinesis.data.item
+
+ return record_handler
+
+
+@pytest.fixture(scope="module")
+def async_kinesis_record_handler_model() -> Callable[..., Awaitable[Any]]:
+ async def record_handler(record: OrderKinesisRecord):
+ if "fail" in record.kinesis.data.item["type"]:
+ raise Exception("Failed to process record.")
+ return record.kinesis.data.item
+
+ return record_handler
+
+
+@pytest.fixture(scope="module")
+def dynamodb_record_handler() -> Callable:
+ def handler(record: DynamoDBRecord):
+ body = record.dynamodb.new_image.get("Message")
+ if "fail" in body:
+ raise ValueError("Failed to process record.")
+ return body
+
+ return handler
+
+
+@pytest.fixture(scope="module")
+def dynamodb_record_handler_model() -> Callable:
+ def record_handler(record: OrderDynamoDBRecord):
+ if "fail" in record.dynamodb.NewImage.Message.item["type"]:
+ raise ValueError("Failed to process record.")
+ return record.dynamodb.NewImage.Message.item
+
+ return record_handler
+
+
+@pytest.fixture(scope="module")
+def async_dynamodb_record_handler() -> Callable[..., Awaitable[Any]]:
+ async def record_handler(record: OrderDynamoDBRecord):
+ if "fail" in record.dynamodb.NewImage.Message.item["type"]:
+ raise ValueError("Failed to process record.")
+ return record.dynamodb.NewImage.Message.item
+
+ return record_handler
+
+
+@pytest.fixture(scope="module")
+def order_event_factory() -> Callable:
+ def factory(item: Dict) -> str:
+ return json.dumps({"item": item})
+
+ return factory
+
+
+def test_batch_processor_context_model(sqs_event_factory, order_event_factory):
+ # GIVEN
+ def record_handler(record: OrderSqs):
+ return record.body.item
+
+ order_event = order_event_factory({"type": "success"})
+ first_record = sqs_event_factory(order_event)
+ second_record = sqs_event_factory(order_event)
+ records = [first_record, second_record]
+
+ # WHEN
+ processor = BatchProcessor(event_type=EventType.SQS, model=OrderSqs)
+ with processor(records, record_handler) as batch:
+ processed_messages = batch.process()
+
+ # THEN
+ order_item = json.loads(order_event)["item"]
+ assert processed_messages == [
+ ("success", order_item, first_record),
+ ("success", order_item, second_record),
+ ]
+
+ assert batch.response() == {"batchItemFailures": []}
+
+
+def test_batch_processor_context_model_with_failure(sqs_event_factory, order_event_factory):
+ # GIVEN
+ def record_handler(record: OrderSqs):
+ if "fail" in record.body.item["type"]:
+ raise Exception("Failed to process record.")
+ return record.body.item
+
+ order_event = order_event_factory({"type": "success"})
+ order_event_fail = order_event_factory({"type": "fail"})
+ first_record = sqs_event_factory(order_event_fail)
+ third_record = sqs_event_factory(order_event_fail)
+ second_record = sqs_event_factory(order_event)
+ records = [first_record, second_record, third_record]
+
+ # WHEN
+ processor = BatchProcessor(event_type=EventType.SQS, model=OrderSqs)
+ with processor(records, record_handler) as batch:
+ batch.process()
+
+ # THEN
+ assert len(batch.fail_messages) == 2
+ assert batch.response() == {
+ "batchItemFailures": [
+ {"itemIdentifier": first_record["messageId"]},
+ {"itemIdentifier": third_record["messageId"]},
+ ],
+ }
+
+
+def test_batch_processor_dynamodb_context_model(dynamodb_event_factory, order_event_factory):
+ # GIVEN
+ class Order(BaseModel):
+ item: dict
+
+ class OrderDynamoDB(BaseModel):
+ Message: Order
+
+ # auto transform json string
+ # so Pydantic can auto-initialize nested Order model
+ @validator("Message", pre=True)
+ def transform_message_to_dict(cls, value: Dict[Literal["S"], str]):
+ return json.loads(value["S"])
+
+ class OrderDynamoDBChangeRecord(DynamoDBStreamChangedRecordModel):
+ NewImage: Optional[OrderDynamoDB] = None
+ OldImage: Optional[OrderDynamoDB] = None
+
+ class OrderDynamoDBRecord(DynamoDBStreamRecordModel):
+ dynamodb: OrderDynamoDBChangeRecord
+
+ def record_handler(record: OrderDynamoDBRecord):
+ return record.dynamodb.NewImage.Message.item
+
+ order_event = order_event_factory({"type": "success"})
+ first_record = dynamodb_event_factory(order_event)
+ second_record = dynamodb_event_factory(order_event)
+ records = [first_record, second_record]
+
+ # WHEN
+ processor = BatchProcessor(event_type=EventType.DynamoDBStreams, model=OrderDynamoDBRecord)
+ with processor(records, record_handler) as batch:
+ processed_messages = batch.process()
+
+ # THEN
+ order_item = json.loads(order_event)["item"]
+ assert processed_messages == [
+ ("success", order_item, first_record),
+ ("success", order_item, second_record),
+ ]
+
+ assert batch.response() == {"batchItemFailures": []}
+
+
+def test_batch_processor_dynamodb_context_model_with_failure(dynamodb_event_factory, order_event_factory):
+ # GIVEN
+ class Order(BaseModel):
+ item: dict
+
+ class OrderDynamoDB(BaseModel):
+ Message: Order
+
+ # auto transform json string
+ # so Pydantic can auto-initialize nested Order model
+ @validator("Message", pre=True)
+ def transform_message_to_dict(cls, value: Dict[Literal["S"], str]):
+ return json.loads(value["S"])
+
+ class OrderDynamoDBChangeRecord(DynamoDBStreamChangedRecordModel):
+ NewImage: Optional[OrderDynamoDB] = None
+ OldImage: Optional[OrderDynamoDB] = None
+
+ class OrderDynamoDBRecord(DynamoDBStreamRecordModel):
+ dynamodb: OrderDynamoDBChangeRecord
+
+ def record_handler(record: OrderDynamoDBRecord):
+ if "fail" in record.dynamodb.NewImage.Message.item["type"]:
+ raise Exception("Failed to process record.")
+ return record.dynamodb.NewImage.Message.item
+
+ order_event = order_event_factory({"type": "success"})
+ order_event_fail = order_event_factory({"type": "fail"})
+ first_record = dynamodb_event_factory(order_event_fail)
+ second_record = dynamodb_event_factory(order_event)
+ third_record = dynamodb_event_factory(order_event_fail)
+ records = [first_record, second_record, third_record]
+
+ # WHEN
+ processor = BatchProcessor(event_type=EventType.DynamoDBStreams, model=OrderDynamoDBRecord)
+ with processor(records, record_handler) as batch:
+ batch.process()
+
+ # THEN
+ assert len(batch.fail_messages) == 2
+ assert batch.response() == {
+ "batchItemFailures": [
+ {"itemIdentifier": first_record["dynamodb"]["SequenceNumber"]},
+ {"itemIdentifier": third_record["dynamodb"]["SequenceNumber"]},
+ ],
+ }
+
+
+def test_batch_processor_kinesis_context_parser_model(
+ kinesis_record_handler_model: Callable,
+ kinesis_event_factory,
+ order_event_factory,
+):
+ # GIVEN
+ order_event = order_event_factory({"type": "success"})
+ first_record = kinesis_event_factory(order_event)
+ second_record = kinesis_event_factory(order_event)
+ records = [first_record, second_record]
+
+ # WHEN
+ processor = BatchProcessor(event_type=EventType.KinesisDataStreams, model=OrderKinesisRecord)
+ with processor(records, kinesis_record_handler_model) as batch:
+ processed_messages = batch.process()
+
+ # THEN
+ order_item = json.loads(order_event)["item"]
+ assert processed_messages == [
+ ("success", order_item, first_record),
+ ("success", order_item, second_record),
+ ]
+
+ assert batch.response() == {"batchItemFailures": []}
+
+
+def test_batch_processor_kinesis_context_parser_model_with_failure(
+ kinesis_record_handler_model: Callable,
+ kinesis_event_factory,
+ order_event_factory,
+):
+ # GIVEN
+ order_event = order_event_factory({"type": "success"})
+ order_event_fail = order_event_factory({"type": "fail"})
+
+ first_record = kinesis_event_factory(order_event_fail)
+ second_record = kinesis_event_factory(order_event)
+ third_record = kinesis_event_factory(order_event_fail)
+ records = [first_record, second_record, third_record]
+
+ # WHEN
+ processor = BatchProcessor(event_type=EventType.KinesisDataStreams, model=OrderKinesisRecord)
+ with processor(records, kinesis_record_handler_model) as batch:
+ batch.process()
+
+ # THEN
+ assert len(batch.fail_messages) == 2
+ assert batch.response() == {
+ "batchItemFailures": [
+ {"itemIdentifier": first_record["kinesis"]["sequenceNumber"]},
+ {"itemIdentifier": third_record["kinesis"]["sequenceNumber"]},
+ ],
+ }
+
+
+def test_sqs_fifo_batch_processor_middleware_with_skip_group_on_error_and_model(sqs_event_fifo_factory, record_handler):
+ # GIVEN a batch of 5 records with 3 different MessageGroupID
+ first_record = SQSRecord(sqs_event_fifo_factory("success", "1"))
+ second_record = SQSRecord(sqs_event_fifo_factory("success", "1"))
+ third_record = SQSRecord(sqs_event_fifo_factory("fail", "2"))
+ fourth_record = SQSRecord(sqs_event_fifo_factory("success", "2"))
+ fifth_record = SQSRecord(sqs_event_fifo_factory("fail", "3"))
+ event = {
+ "Records": [
+ first_record.raw_event,
+ second_record.raw_event,
+ third_record.raw_event,
+ fourth_record.raw_event,
+ fifth_record.raw_event,
+ ],
+ }
+
+ class OrderSqsRecord(SqsRecordModel):
+ receiptHandle: str
+
+ # WHEN the FIFO processor is set to continue processing even after encountering errors in specific MessageGroupID
+ # WHEN processor is using a Pydantic Model we must be able to access MessageGroupID property
+ processor = SqsFifoPartialProcessor(skip_group_on_error=True, model=OrderSqsRecord)
+
+ def record_handler(record: OrderSqsRecord):
+ if record.body == "fail":
+ raise ValueError("blah")
+
+ @batch_processor(record_handler=record_handler, processor=processor)
+ def lambda_handler(event, context):
+ return processor.response()
+
+ # WHEN
+ result = lambda_handler(event, {})
+
+ # THEN only failed messages should originate from MessageGroupID 3
+ assert len(result["batchItemFailures"]) == 3
+ assert result["batchItemFailures"][0]["itemIdentifier"] == third_record.message_id
+ assert result["batchItemFailures"][1]["itemIdentifier"] == fourth_record.message_id
+ assert result["batchItemFailures"][2]["itemIdentifier"] == fifth_record.message_id
+
+
+def test_batch_processor_model_with_partial_validation_error(
+ record_handler_model: Callable,
+ sqs_event_factory,
+ order_event_factory,
+):
+ # GIVEN
+ order_event = order_event_factory({"type": "success"})
+ first_record = sqs_event_factory(order_event)
+ second_record = sqs_event_factory(order_event)
+ malformed_record = sqs_event_factory({"poison": "pill"})
+ records = [first_record, malformed_record, second_record]
+
+ # WHEN
+ processor = BatchProcessor(event_type=EventType.SQS, model=OrderSqs)
+ with processor(records, record_handler_model) as batch:
+ batch.process()
+
+ # THEN
+ assert len(batch.fail_messages) == 1
+ assert batch.response() == {
+ "batchItemFailures": [
+ {"itemIdentifier": malformed_record["messageId"]},
+ ],
+ }
+
+
+def test_batch_processor_dynamodb_context_model_with_partial_validation_error(
+ dynamodb_record_handler_model: Callable,
+ dynamodb_event_factory,
+ order_event_factory,
+):
+ # GIVEN
+ order_event = order_event_factory({"type": "success"})
+ first_record = dynamodb_event_factory(order_event)
+ second_record = dynamodb_event_factory(order_event)
+ malformed_record = dynamodb_event_factory({"poison": "pill"})
+ records = [first_record, malformed_record, second_record]
+
+ # WHEN
+ processor = BatchProcessor(event_type=EventType.DynamoDBStreams, model=OrderDynamoDBRecord)
+ with processor(records, dynamodb_record_handler_model) as batch:
+ batch.process()
+
+ # THEN
+ assert len(batch.fail_messages) == 1
+ assert batch.response() == {
+ "batchItemFailures": [
+ {"itemIdentifier": malformed_record["dynamodb"]["SequenceNumber"]},
+ ],
+ }
+
+
+def test_batch_processor_kinesis_context_parser_model_with_partial_validation_error(
+ kinesis_record_handler_model: Callable,
+ kinesis_event_factory,
+ order_event_factory,
+):
+ # GIVEN
+ order_event = order_event_factory({"type": "success"})
+ first_record = kinesis_event_factory(order_event)
+ second_record = kinesis_event_factory(order_event)
+ malformed_record = kinesis_event_factory('{"poison": "pill"}')
+ records = [first_record, malformed_record, second_record]
+
+ # WHEN
+ processor = BatchProcessor(event_type=EventType.KinesisDataStreams, model=OrderKinesisRecord)
+ with processor(records, kinesis_record_handler_model) as batch:
+ batch.process()
+
+ # THEN
+ assert len(batch.fail_messages) == 1
+ assert batch.response() == {
+ "batchItemFailures": [
+ {"itemIdentifier": malformed_record["kinesis"]["sequenceNumber"]},
+ ],
+ }
+
+
+def test_async_batch_processor_model_with_partial_validation_error(
+ async_record_handler_model: Callable,
+ sqs_event_factory,
+ order_event_factory,
+):
+ # GIVEN
+ order_event = order_event_factory({"type": "success"})
+ first_record = sqs_event_factory(order_event)
+ second_record = sqs_event_factory(order_event)
+ malformed_record = sqs_event_factory({"poison": "pill"})
+ records = [first_record, malformed_record, second_record]
+
+ # WHEN
+ processor = AsyncBatchProcessor(event_type=EventType.SQS, model=OrderSqs)
+ with processor(records, async_record_handler_model) as batch:
+ batch.async_process()
+
+ # THEN
+ assert len(batch.fail_messages) == 1
+ assert batch.response() == {
+ "batchItemFailures": [
+ {"itemIdentifier": malformed_record["messageId"]},
+ ],
+ }
+
+
+def test_async_batch_processor_dynamodb_context_model_with_partial_validation_error(
+ async_dynamodb_record_handler: Callable,
+ dynamodb_event_factory,
+ order_event_factory,
+):
+ # GIVEN
+ order_event = order_event_factory({"type": "success"})
+ first_record = dynamodb_event_factory(order_event)
+ second_record = dynamodb_event_factory(order_event)
+ malformed_record = dynamodb_event_factory({"poison": "pill"})
+ records = [first_record, malformed_record, second_record]
+
+ # WHEN
+ processor = AsyncBatchProcessor(event_type=EventType.DynamoDBStreams, model=OrderDynamoDBRecord)
+ with processor(records, async_dynamodb_record_handler) as batch:
+ batch.async_process()
+
+ # THEN
+ assert len(batch.fail_messages) == 1
+ assert batch.response() == {
+ "batchItemFailures": [
+ {"itemIdentifier": malformed_record["dynamodb"]["SequenceNumber"]},
+ ],
+ }
+
+
+def test_async_batch_processor_kinesis_context_parser_model_with_partial_validation_error(
+ async_kinesis_record_handler_model: Callable,
+ kinesis_event_factory,
+ order_event_factory,
+):
+ # GIVEN
+ order_event = order_event_factory({"type": "success"})
+ first_record = kinesis_event_factory(order_event)
+ second_record = kinesis_event_factory(order_event)
+ malformed_record = kinesis_event_factory('{"poison": "pill"}')
+ records = [first_record, malformed_record, second_record]
+
+ # WHEN
+ processor = AsyncBatchProcessor(event_type=EventType.KinesisDataStreams, model=OrderKinesisRecord)
+ with processor(records, async_kinesis_record_handler_model) as batch:
+ batch.async_process()
+
+ # THEN
+ assert len(batch.fail_messages) == 1
+ assert batch.response() == {
+ "batchItemFailures": [
+ {"itemIdentifier": malformed_record["kinesis"]["sequenceNumber"]},
+ ],
+ }
diff --git a/tests/functional/batch/required_dependencies/__init__.py b/tests/functional/batch/required_dependencies/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/tests/functional/test_utilities_batch.py b/tests/functional/batch/required_dependencies/test_utilities_batch.py
similarity index 58%
rename from tests/functional/test_utilities_batch.py
rename to tests/functional/batch/required_dependencies/test_utilities_batch.py
index 8ea2fac7bc5..732e2f0ef78 100644
--- a/tests/functional/test_utilities_batch.py
+++ b/tests/functional/batch/required_dependencies/test_utilities_batch.py
@@ -1,10 +1,9 @@
import json
import uuid
from random import randint
-from typing import Any, Awaitable, Callable, Dict, Optional
+from typing import Any, Awaitable, Callable, Dict
import pytest
-from botocore.config import Config
from aws_lambda_powertools.utilities.batch import (
AsyncBatchProcessor,
@@ -24,18 +23,6 @@
KinesisStreamRecord,
)
from aws_lambda_powertools.utilities.data_classes.sqs_event import SQSRecord
-from aws_lambda_powertools.utilities.parser import BaseModel, validator
-from aws_lambda_powertools.utilities.parser.models import (
- DynamoDBStreamChangedRecordModel,
- DynamoDBStreamRecordModel,
- SqsRecordModel,
-)
-from aws_lambda_powertools.utilities.parser.types import Literal
-from tests.functional.batch.sample_models import (
- OrderDynamoDBRecord,
- OrderKinesisRecord,
- OrderSqs,
-)
from tests.functional.utils import b64_to_str, str_to_b64
@@ -146,16 +133,6 @@ def handler(record):
return handler
-@pytest.fixture(scope="module")
-def record_handler_model() -> Callable:
- def record_handler(record: OrderSqs):
- if "fail" in record.body.item["type"]:
- raise Exception("Failed to process record.")
- return record.body.item
-
- return record_handler
-
-
@pytest.fixture(scope="module")
def async_record_handler() -> Callable[..., Awaitable[Any]]:
async def handler(record):
@@ -167,16 +144,6 @@ async def handler(record):
return handler
-@pytest.fixture(scope="module")
-def async_record_handler_model() -> Callable[..., Awaitable[Any]]:
- async def async_record_handler(record: OrderSqs):
- if "fail" in record.body.item["type"]:
- raise ValueError("Failed to process record.")
- return record.body.item
-
- return async_record_handler
-
-
@pytest.fixture(scope="module")
def kinesis_record_handler() -> Callable:
def handler(record: KinesisStreamRecord):
@@ -188,26 +155,6 @@ def handler(record: KinesisStreamRecord):
return handler
-@pytest.fixture(scope="module")
-def kinesis_record_handler_model() -> Callable:
- def record_handler(record: OrderKinesisRecord):
- if "fail" in record.kinesis.data.item["type"]:
- raise ValueError("Failed to process record.")
- return record.kinesis.data.item
-
- return record_handler
-
-
-@pytest.fixture(scope="module")
-def async_kinesis_record_handler_model() -> Callable[..., Awaitable[Any]]:
- async def record_handler(record: OrderKinesisRecord):
- if "fail" in record.kinesis.data.item["type"]:
- raise Exception("Failed to process record.")
- return record.kinesis.data.item
-
- return record_handler
-
-
@pytest.fixture(scope="module")
def dynamodb_record_handler() -> Callable:
def handler(record: DynamoDBRecord):
@@ -219,31 +166,6 @@ def handler(record: DynamoDBRecord):
return handler
-@pytest.fixture(scope="module")
-def dynamodb_record_handler_model() -> Callable:
- def record_handler(record: OrderDynamoDBRecord):
- if "fail" in record.dynamodb.NewImage.Message.item["type"]:
- raise ValueError("Failed to process record.")
- return record.dynamodb.NewImage.Message.item
-
- return record_handler
-
-
-@pytest.fixture(scope="module")
-def async_dynamodb_record_handler() -> Callable[..., Awaitable[Any]]:
- async def record_handler(record: OrderDynamoDBRecord):
- if "fail" in record.dynamodb.NewImage.Message.item["type"]:
- raise ValueError("Failed to process record.")
- return record.dynamodb.NewImage.Message.item
-
- return record_handler
-
-
-@pytest.fixture(scope="module")
-def config() -> Config:
- return Config(region_name="us-east-1")
-
-
@pytest.fixture(scope="module")
def order_event_factory() -> Callable:
def factory(item: Dict) -> str:
@@ -459,207 +381,6 @@ def lambda_handler(event, context):
assert len(result["batchItemFailures"]) == 2
-def test_batch_processor_context_model(sqs_event_factory, order_event_factory):
- # GIVEN
- def record_handler(record: OrderSqs):
- return record.body.item
-
- order_event = order_event_factory({"type": "success"})
- first_record = sqs_event_factory(order_event)
- second_record = sqs_event_factory(order_event)
- records = [first_record, second_record]
-
- # WHEN
- processor = BatchProcessor(event_type=EventType.SQS, model=OrderSqs)
- with processor(records, record_handler) as batch:
- processed_messages = batch.process()
-
- # THEN
- order_item = json.loads(order_event)["item"]
- assert processed_messages == [
- ("success", order_item, first_record),
- ("success", order_item, second_record),
- ]
-
- assert batch.response() == {"batchItemFailures": []}
-
-
-def test_batch_processor_context_model_with_failure(sqs_event_factory, order_event_factory):
- # GIVEN
- def record_handler(record: OrderSqs):
- if "fail" in record.body.item["type"]:
- raise Exception("Failed to process record.")
- return record.body.item
-
- order_event = order_event_factory({"type": "success"})
- order_event_fail = order_event_factory({"type": "fail"})
- first_record = sqs_event_factory(order_event_fail)
- third_record = sqs_event_factory(order_event_fail)
- second_record = sqs_event_factory(order_event)
- records = [first_record, second_record, third_record]
-
- # WHEN
- processor = BatchProcessor(event_type=EventType.SQS, model=OrderSqs)
- with processor(records, record_handler) as batch:
- batch.process()
-
- # THEN
- assert len(batch.fail_messages) == 2
- assert batch.response() == {
- "batchItemFailures": [
- {"itemIdentifier": first_record["messageId"]},
- {"itemIdentifier": third_record["messageId"]},
- ],
- }
-
-
-def test_batch_processor_dynamodb_context_model(dynamodb_event_factory, order_event_factory):
- # GIVEN
- class Order(BaseModel):
- item: dict
-
- class OrderDynamoDB(BaseModel):
- Message: Order
-
- # auto transform json string
- # so Pydantic can auto-initialize nested Order model
- @validator("Message", pre=True)
- def transform_message_to_dict(cls, value: Dict[Literal["S"], str]):
- return json.loads(value["S"])
-
- class OrderDynamoDBChangeRecord(DynamoDBStreamChangedRecordModel):
- NewImage: Optional[OrderDynamoDB] = None
- OldImage: Optional[OrderDynamoDB] = None
-
- class OrderDynamoDBRecord(DynamoDBStreamRecordModel):
- dynamodb: OrderDynamoDBChangeRecord
-
- def record_handler(record: OrderDynamoDBRecord):
- return record.dynamodb.NewImage.Message.item
-
- order_event = order_event_factory({"type": "success"})
- first_record = dynamodb_event_factory(order_event)
- second_record = dynamodb_event_factory(order_event)
- records = [first_record, second_record]
-
- # WHEN
- processor = BatchProcessor(event_type=EventType.DynamoDBStreams, model=OrderDynamoDBRecord)
- with processor(records, record_handler) as batch:
- processed_messages = batch.process()
-
- # THEN
- order_item = json.loads(order_event)["item"]
- assert processed_messages == [
- ("success", order_item, first_record),
- ("success", order_item, second_record),
- ]
-
- assert batch.response() == {"batchItemFailures": []}
-
-
-def test_batch_processor_dynamodb_context_model_with_failure(dynamodb_event_factory, order_event_factory):
- # GIVEN
- class Order(BaseModel):
- item: dict
-
- class OrderDynamoDB(BaseModel):
- Message: Order
-
- # auto transform json string
- # so Pydantic can auto-initialize nested Order model
- @validator("Message", pre=True)
- def transform_message_to_dict(cls, value: Dict[Literal["S"], str]):
- return json.loads(value["S"])
-
- class OrderDynamoDBChangeRecord(DynamoDBStreamChangedRecordModel):
- NewImage: Optional[OrderDynamoDB] = None
- OldImage: Optional[OrderDynamoDB] = None
-
- class OrderDynamoDBRecord(DynamoDBStreamRecordModel):
- dynamodb: OrderDynamoDBChangeRecord
-
- def record_handler(record: OrderDynamoDBRecord):
- if "fail" in record.dynamodb.NewImage.Message.item["type"]:
- raise Exception("Failed to process record.")
- return record.dynamodb.NewImage.Message.item
-
- order_event = order_event_factory({"type": "success"})
- order_event_fail = order_event_factory({"type": "fail"})
- first_record = dynamodb_event_factory(order_event_fail)
- second_record = dynamodb_event_factory(order_event)
- third_record = dynamodb_event_factory(order_event_fail)
- records = [first_record, second_record, third_record]
-
- # WHEN
- processor = BatchProcessor(event_type=EventType.DynamoDBStreams, model=OrderDynamoDBRecord)
- with processor(records, record_handler) as batch:
- batch.process()
-
- # THEN
- assert len(batch.fail_messages) == 2
- assert batch.response() == {
- "batchItemFailures": [
- {"itemIdentifier": first_record["dynamodb"]["SequenceNumber"]},
- {"itemIdentifier": third_record["dynamodb"]["SequenceNumber"]},
- ],
- }
-
-
-def test_batch_processor_kinesis_context_parser_model(
- kinesis_record_handler_model: Callable,
- kinesis_event_factory,
- order_event_factory,
-):
- # GIVEN
- order_event = order_event_factory({"type": "success"})
- first_record = kinesis_event_factory(order_event)
- second_record = kinesis_event_factory(order_event)
- records = [first_record, second_record]
-
- # WHEN
- processor = BatchProcessor(event_type=EventType.KinesisDataStreams, model=OrderKinesisRecord)
- with processor(records, kinesis_record_handler_model) as batch:
- processed_messages = batch.process()
-
- # THEN
- order_item = json.loads(order_event)["item"]
- assert processed_messages == [
- ("success", order_item, first_record),
- ("success", order_item, second_record),
- ]
-
- assert batch.response() == {"batchItemFailures": []}
-
-
-def test_batch_processor_kinesis_context_parser_model_with_failure(
- kinesis_record_handler_model: Callable,
- kinesis_event_factory,
- order_event_factory,
-):
- # GIVEN
- order_event = order_event_factory({"type": "success"})
- order_event_fail = order_event_factory({"type": "fail"})
-
- first_record = kinesis_event_factory(order_event_fail)
- second_record = kinesis_event_factory(order_event)
- third_record = kinesis_event_factory(order_event_fail)
- records = [first_record, second_record, third_record]
-
- # WHEN
- processor = BatchProcessor(event_type=EventType.KinesisDataStreams, model=OrderKinesisRecord)
- with processor(records, kinesis_record_handler_model) as batch:
- batch.process()
-
- # THEN
- assert len(batch.fail_messages) == 2
- assert batch.response() == {
- "batchItemFailures": [
- {"itemIdentifier": first_record["kinesis"]["sequenceNumber"]},
- {"itemIdentifier": third_record["kinesis"]["sequenceNumber"]},
- ],
- }
-
-
def test_batch_processor_error_when_entire_batch_fails(sqs_event_factory, record_handler):
# GIVEN
first_record = SQSRecord(sqs_event_factory("fail"))
@@ -801,48 +522,6 @@ def lambda_handler(event, context):
assert result["batchItemFailures"][3]["itemIdentifier"] == fourth_record.message_id
-def test_sqs_fifo_batch_processor_middleware_with_skip_group_on_error_and_model(sqs_event_fifo_factory, record_handler):
- # GIVEN a batch of 5 records with 3 different MessageGroupID
- first_record = SQSRecord(sqs_event_fifo_factory("success", "1"))
- second_record = SQSRecord(sqs_event_fifo_factory("success", "1"))
- third_record = SQSRecord(sqs_event_fifo_factory("fail", "2"))
- fourth_record = SQSRecord(sqs_event_fifo_factory("success", "2"))
- fifth_record = SQSRecord(sqs_event_fifo_factory("fail", "3"))
- event = {
- "Records": [
- first_record.raw_event,
- second_record.raw_event,
- third_record.raw_event,
- fourth_record.raw_event,
- fifth_record.raw_event,
- ],
- }
-
- class OrderSqsRecord(SqsRecordModel):
- receiptHandle: str
-
- # WHEN the FIFO processor is set to continue processing even after encountering errors in specific MessageGroupID
- # WHEN processor is using a Pydantic Model we must be able to access MessageGroupID property
- processor = SqsFifoPartialProcessor(skip_group_on_error=True, model=OrderSqsRecord)
-
- def record_handler(record: OrderSqsRecord):
- if record.body == "fail":
- raise ValueError("blah")
-
- @batch_processor(record_handler=record_handler, processor=processor)
- def lambda_handler(event, context):
- return processor.response()
-
- # WHEN
- result = lambda_handler(event, {})
-
- # THEN only failed messages should originate from MessageGroupID 3
- assert len(result["batchItemFailures"]) == 3
- assert result["batchItemFailures"][0]["itemIdentifier"] == third_record.message_id
- assert result["batchItemFailures"][1]["itemIdentifier"] == fourth_record.message_id
- assert result["batchItemFailures"][2]["itemIdentifier"] == fifth_record.message_id
-
-
def test_async_batch_processor_middleware_success_only(sqs_event_factory, async_record_handler):
# GIVEN
first_record = SQSRecord(sqs_event_factory("success"))
@@ -984,159 +663,3 @@ def test_async_process_partial_response_invalid_input(async_record_handler: Call
# WHEN/THEN
with pytest.raises(ValueError):
async_process_partial_response(batch, record_handler, processor)
-
-
-def test_batch_processor_model_with_partial_validation_error(
- record_handler_model: Callable,
- sqs_event_factory,
- order_event_factory,
-):
- # GIVEN
- order_event = order_event_factory({"type": "success"})
- first_record = sqs_event_factory(order_event)
- second_record = sqs_event_factory(order_event)
- malformed_record = sqs_event_factory({"poison": "pill"})
- records = [first_record, malformed_record, second_record]
-
- # WHEN
- processor = BatchProcessor(event_type=EventType.SQS, model=OrderSqs)
- with processor(records, record_handler_model) as batch:
- batch.process()
-
- # THEN
- assert len(batch.fail_messages) == 1
- assert batch.response() == {
- "batchItemFailures": [
- {"itemIdentifier": malformed_record["messageId"]},
- ],
- }
-
-
-def test_batch_processor_dynamodb_context_model_with_partial_validation_error(
- dynamodb_record_handler_model: Callable,
- dynamodb_event_factory,
- order_event_factory,
-):
- # GIVEN
- order_event = order_event_factory({"type": "success"})
- first_record = dynamodb_event_factory(order_event)
- second_record = dynamodb_event_factory(order_event)
- malformed_record = dynamodb_event_factory({"poison": "pill"})
- records = [first_record, malformed_record, second_record]
-
- # WHEN
- processor = BatchProcessor(event_type=EventType.DynamoDBStreams, model=OrderDynamoDBRecord)
- with processor(records, dynamodb_record_handler_model) as batch:
- batch.process()
-
- # THEN
- assert len(batch.fail_messages) == 1
- assert batch.response() == {
- "batchItemFailures": [
- {"itemIdentifier": malformed_record["dynamodb"]["SequenceNumber"]},
- ],
- }
-
-
-def test_batch_processor_kinesis_context_parser_model_with_partial_validation_error(
- kinesis_record_handler_model: Callable,
- kinesis_event_factory,
- order_event_factory,
-):
- # GIVEN
- order_event = order_event_factory({"type": "success"})
- first_record = kinesis_event_factory(order_event)
- second_record = kinesis_event_factory(order_event)
- malformed_record = kinesis_event_factory('{"poison": "pill"}')
- records = [first_record, malformed_record, second_record]
-
- # WHEN
- processor = BatchProcessor(event_type=EventType.KinesisDataStreams, model=OrderKinesisRecord)
- with processor(records, kinesis_record_handler_model) as batch:
- batch.process()
-
- # THEN
- assert len(batch.fail_messages) == 1
- assert batch.response() == {
- "batchItemFailures": [
- {"itemIdentifier": malformed_record["kinesis"]["sequenceNumber"]},
- ],
- }
-
-
-def test_async_batch_processor_model_with_partial_validation_error(
- async_record_handler_model: Callable,
- sqs_event_factory,
- order_event_factory,
-):
- # GIVEN
- order_event = order_event_factory({"type": "success"})
- first_record = sqs_event_factory(order_event)
- second_record = sqs_event_factory(order_event)
- malformed_record = sqs_event_factory({"poison": "pill"})
- records = [first_record, malformed_record, second_record]
-
- # WHEN
- processor = AsyncBatchProcessor(event_type=EventType.SQS, model=OrderSqs)
- with processor(records, async_record_handler_model) as batch:
- batch.async_process()
-
- # THEN
- assert len(batch.fail_messages) == 1
- assert batch.response() == {
- "batchItemFailures": [
- {"itemIdentifier": malformed_record["messageId"]},
- ],
- }
-
-
-def test_async_batch_processor_dynamodb_context_model_with_partial_validation_error(
- async_dynamodb_record_handler: Callable,
- dynamodb_event_factory,
- order_event_factory,
-):
- # GIVEN
- order_event = order_event_factory({"type": "success"})
- first_record = dynamodb_event_factory(order_event)
- second_record = dynamodb_event_factory(order_event)
- malformed_record = dynamodb_event_factory({"poison": "pill"})
- records = [first_record, malformed_record, second_record]
-
- # WHEN
- processor = AsyncBatchProcessor(event_type=EventType.DynamoDBStreams, model=OrderDynamoDBRecord)
- with processor(records, async_dynamodb_record_handler) as batch:
- batch.async_process()
-
- # THEN
- assert len(batch.fail_messages) == 1
- assert batch.response() == {
- "batchItemFailures": [
- {"itemIdentifier": malformed_record["dynamodb"]["SequenceNumber"]},
- ],
- }
-
-
-def test_async_batch_processor_kinesis_context_parser_model_with_partial_validation_error(
- async_kinesis_record_handler_model: Callable,
- kinesis_event_factory,
- order_event_factory,
-):
- # GIVEN
- order_event = order_event_factory({"type": "success"})
- first_record = kinesis_event_factory(order_event)
- second_record = kinesis_event_factory(order_event)
- malformed_record = kinesis_event_factory('{"poison": "pill"}')
- records = [first_record, malformed_record, second_record]
-
- # WHEN
- processor = AsyncBatchProcessor(event_type=EventType.KinesisDataStreams, model=OrderKinesisRecord)
- with processor(records, async_kinesis_record_handler_model) as batch:
- batch.async_process()
-
- # THEN
- assert len(batch.fail_messages) == 1
- assert batch.response() == {
- "batchItemFailures": [
- {"itemIdentifier": malformed_record["kinesis"]["sequenceNumber"]},
- ],
- }
diff --git a/tests/functional/data_masking/_aws_encryption_sdk/__init__.py b/tests/functional/data_masking/_aws_encryption_sdk/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/tests/functional/data_masking/test_aws_encryption_sdk.py b/tests/functional/data_masking/_aws_encryption_sdk/test_aws_encryption_sdk.py
similarity index 100%
rename from tests/functional/data_masking/test_aws_encryption_sdk.py
rename to tests/functional/data_masking/_aws_encryption_sdk/test_aws_encryption_sdk.py
diff --git a/tests/functional/event_handler/_pydantic/__init__.py b/tests/functional/event_handler/_pydantic/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/tests/functional/event_handler/conftest.py b/tests/functional/event_handler/_pydantic/conftest.py
similarity index 100%
rename from tests/functional/event_handler/conftest.py
rename to tests/functional/event_handler/_pydantic/conftest.py
diff --git a/tests/functional/event_handler/_pydantic/test_api_gateway.py b/tests/functional/event_handler/_pydantic/test_api_gateway.py
new file mode 100644
index 00000000000..dcd05c4f1f7
--- /dev/null
+++ b/tests/functional/event_handler/_pydantic/test_api_gateway.py
@@ -0,0 +1,80 @@
+from pydantic import BaseModel
+
+from aws_lambda_powertools.event_handler import content_types
+from aws_lambda_powertools.event_handler.api_gateway import (
+ ApiGatewayResolver,
+ Response,
+)
+from aws_lambda_powertools.event_handler.openapi.exceptions import RequestValidationError
+from tests.functional.utils import load_event
+
+LOAD_GW_EVENT = load_event("apiGatewayProxyEvent.json")
+
+
+def test_exception_handler_with_data_validation():
+ # GIVEN a resolver with an exception handler defined for RequestValidationError
+ app = ApiGatewayResolver(enable_validation=True)
+
+ @app.exception_handler(RequestValidationError)
+ def handle_validation_error(ex: RequestValidationError):
+ return Response(
+ status_code=422,
+ content_type=content_types.TEXT_PLAIN,
+ body=f"Invalid data. Number of errors: {len(ex.errors())}",
+ )
+
+ @app.get("/my/path")
+ def get_lambda(param: int): ...
+
+ # WHEN calling the event handler
+ # AND a RequestValidationError is raised
+ result = app(LOAD_GW_EVENT, {})
+
+ # THEN call the exception_handler
+ assert result["statusCode"] == 422
+ assert result["multiValueHeaders"]["Content-Type"] == [content_types.TEXT_PLAIN]
+ assert result["body"] == "Invalid data. Number of errors: 1"
+
+
+def test_exception_handler_with_data_validation_pydantic_response():
+ # GIVEN a resolver with an exception handler defined for RequestValidationError
+ app = ApiGatewayResolver(enable_validation=True)
+
+ class Err(BaseModel):
+ msg: str
+
+ @app.exception_handler(RequestValidationError)
+ def handle_validation_error(ex: RequestValidationError):
+ return Response(
+ status_code=422,
+ content_type=content_types.APPLICATION_JSON,
+ body=Err(msg=f"Invalid data. Number of errors: {len(ex.errors())}"),
+ )
+
+ @app.get("/my/path")
+ def get_lambda(param: int): ...
+
+ # WHEN calling the event handler
+ # AND a RequestValidationError is raised
+ result = app(LOAD_GW_EVENT, {})
+
+ # THEN exception handler's pydantic response should be serialized correctly
+ assert result["statusCode"] == 422
+ assert result["body"] == '{"msg":"Invalid data. Number of errors: 1"}'
+
+
+def test_data_validation_error():
+ # GIVEN a resolver without an exception handler
+ app = ApiGatewayResolver(enable_validation=True)
+
+ @app.get("/my/path")
+ def get_lambda(param: int): ...
+
+ # WHEN calling the event handler
+ # AND a RequestValidationError is raised
+ result = app(LOAD_GW_EVENT, {})
+
+ # THEN call the exception_handler
+ assert result["statusCode"] == 422
+ assert result["multiValueHeaders"]["Content-Type"] == [content_types.APPLICATION_JSON]
+ assert "missing" in result["body"]
diff --git a/tests/functional/event_handler/test_bedrock_agent.py b/tests/functional/event_handler/_pydantic/test_bedrock_agent.py
similarity index 100%
rename from tests/functional/event_handler/test_bedrock_agent.py
rename to tests/functional/event_handler/_pydantic/test_bedrock_agent.py
diff --git a/tests/functional/event_handler/test_openapi_encoders.py b/tests/functional/event_handler/_pydantic/test_openapi_encoders.py
similarity index 100%
rename from tests/functional/event_handler/test_openapi_encoders.py
rename to tests/functional/event_handler/_pydantic/test_openapi_encoders.py
diff --git a/tests/functional/event_handler/test_openapi_params.py b/tests/functional/event_handler/_pydantic/test_openapi_params.py
similarity index 100%
rename from tests/functional/event_handler/test_openapi_params.py
rename to tests/functional/event_handler/_pydantic/test_openapi_params.py
diff --git a/tests/functional/event_handler/test_openapi_responses.py b/tests/functional/event_handler/_pydantic/test_openapi_responses.py
similarity index 100%
rename from tests/functional/event_handler/test_openapi_responses.py
rename to tests/functional/event_handler/_pydantic/test_openapi_responses.py
diff --git a/tests/functional/event_handler/test_openapi_schema_pydantic_v1.py b/tests/functional/event_handler/_pydantic/test_openapi_schema_pydantic_v1.py
similarity index 100%
rename from tests/functional/event_handler/test_openapi_schema_pydantic_v1.py
rename to tests/functional/event_handler/_pydantic/test_openapi_schema_pydantic_v1.py
diff --git a/tests/functional/event_handler/test_openapi_schema_pydantic_v2.py b/tests/functional/event_handler/_pydantic/test_openapi_schema_pydantic_v2.py
similarity index 100%
rename from tests/functional/event_handler/test_openapi_schema_pydantic_v2.py
rename to tests/functional/event_handler/_pydantic/test_openapi_schema_pydantic_v2.py
diff --git a/tests/functional/event_handler/test_openapi_security.py b/tests/functional/event_handler/_pydantic/test_openapi_security.py
similarity index 100%
rename from tests/functional/event_handler/test_openapi_security.py
rename to tests/functional/event_handler/_pydantic/test_openapi_security.py
diff --git a/tests/functional/event_handler/test_openapi_security_schemes.py b/tests/functional/event_handler/_pydantic/test_openapi_security_schemes.py
similarity index 100%
rename from tests/functional/event_handler/test_openapi_security_schemes.py
rename to tests/functional/event_handler/_pydantic/test_openapi_security_schemes.py
diff --git a/tests/functional/event_handler/test_openapi_serialization.py b/tests/functional/event_handler/_pydantic/test_openapi_serialization.py
similarity index 100%
rename from tests/functional/event_handler/test_openapi_serialization.py
rename to tests/functional/event_handler/_pydantic/test_openapi_serialization.py
diff --git a/tests/functional/event_handler/test_openapi_servers.py b/tests/functional/event_handler/_pydantic/test_openapi_servers.py
similarity index 100%
rename from tests/functional/event_handler/test_openapi_servers.py
rename to tests/functional/event_handler/_pydantic/test_openapi_servers.py
diff --git a/tests/functional/event_handler/test_openapi_swagger.py b/tests/functional/event_handler/_pydantic/test_openapi_swagger.py
similarity index 100%
rename from tests/functional/event_handler/test_openapi_swagger.py
rename to tests/functional/event_handler/_pydantic/test_openapi_swagger.py
diff --git a/tests/functional/event_handler/test_openapi_tags.py b/tests/functional/event_handler/_pydantic/test_openapi_tags.py
similarity index 100%
rename from tests/functional/event_handler/test_openapi_tags.py
rename to tests/functional/event_handler/_pydantic/test_openapi_tags.py
diff --git a/tests/functional/event_handler/test_openapi_validation_middleware.py b/tests/functional/event_handler/_pydantic/test_openapi_validation_middleware.py
similarity index 100%
rename from tests/functional/event_handler/test_openapi_validation_middleware.py
rename to tests/functional/event_handler/_pydantic/test_openapi_validation_middleware.py
diff --git a/tests/functional/event_handler/required_dependencies/__init__.py b/tests/functional/event_handler/required_dependencies/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/tests/functional/event_handler/required_dependencies/appsync/__init__.py b/tests/functional/event_handler/required_dependencies/appsync/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/tests/functional/event_handler/required_dependencies/appsync/test_appsync_batch_resolvers.py b/tests/functional/event_handler/required_dependencies/appsync/test_appsync_batch_resolvers.py
new file mode 100644
index 00000000000..cc78cbf0f9c
--- /dev/null
+++ b/tests/functional/event_handler/required_dependencies/appsync/test_appsync_batch_resolvers.py
@@ -0,0 +1,909 @@
+from typing import List, Optional
+
+import pytest
+
+from aws_lambda_powertools.event_handler import AppSyncResolver
+from aws_lambda_powertools.event_handler.graphql_appsync.exceptions import InvalidBatchResponse, ResolverNotFoundError
+from aws_lambda_powertools.event_handler.graphql_appsync.router import Router
+from aws_lambda_powertools.utilities.data_classes import AppSyncResolverEvent
+from aws_lambda_powertools.utilities.typing import LambdaContext
+from aws_lambda_powertools.warnings import PowertoolsUserWarning
+
+
+# TESTS RECEIVING THE EVENT PARTIALLY AND PROCESS EACH RECORD PER TIME.
+def test_resolve_batch_processing_with_related_events_one_at_time():
+ # GIVEN An event with multiple requests to fetch related posts for different post IDs.
+ event = [
+ {
+ "arguments": {},
+ "identity": "None",
+ "source": {
+ "post_id": "3",
+ "title": "Third book",
+ },
+ "info": {
+ "selectionSetList": [
+ "title",
+ ],
+ "selectionSetGraphQL": "{\n title\n}",
+ "fieldName": "relatedPosts",
+ "parentTypeName": "Post",
+ },
+ },
+ {
+ "arguments": {},
+ "identity": "None",
+ "source": {
+ "post_id": "4",
+ "title": "Fifth book",
+ },
+ "info": {
+ "selectionSetList": [
+ "title",
+ ],
+ "selectionSetGraphQL": "{\n title\n}",
+ "fieldName": "relatedPosts",
+ "parentTypeName": "Post",
+ },
+ },
+ {
+ "arguments": {},
+ "identity": "None",
+ "source": {
+ "post_id": "1",
+ "title": "First book",
+ },
+ "info": {
+ "selectionSetList": [
+ "title",
+ ],
+ "selectionSetGraphQL": "{\n title\n}",
+ "fieldName": "relatedPosts",
+ "parentTypeName": "Post",
+ },
+ },
+ ]
+
+ # GIVEN A dictionary of posts and a dictionary of related posts.
+ posts = {
+ "1": {
+ "post_id": "1",
+ "title": "First book",
+ },
+ "2": {
+ "post_id": "2",
+ "title": "Second book",
+ },
+ "3": {
+ "post_id": "3",
+ "title": "Third book",
+ },
+ "4": {
+ "post_id": "4",
+ "title": "Fourth book",
+ },
+ }
+
+ posts_related = {
+ "1": [posts["2"]],
+ "2": [posts["3"], posts["4"], posts["1"]],
+ "3": [posts["2"], posts["1"]],
+ "4": [posts["3"], posts["1"]],
+ }
+
+ app = AppSyncResolver()
+
+ @app.batch_resolver(type_name="Post", field_name="relatedPosts", aggregate=False)
+ def related_posts(event: AppSyncResolverEvent) -> Optional[list]:
+ return posts_related[event.source["post_id"]]
+
+ # WHEN related_posts function, which is the batch resolver, is called with the event.
+ result = app.resolve(event, LambdaContext())
+
+ # THEN the result must be a list of related posts
+ assert result == [
+ posts_related["3"],
+ posts_related["4"],
+ posts_related["1"],
+ ]
+
+
+# Batch resolver tests
+def test_resolve_batch_processing_with_simple_queries_one_at_time():
+ # GIVEN a list of events representing GraphQL queries for listing locations
+ event = [
+ {
+ "typeName": "Query",
+ "info": {
+ "fieldName": "listLocations",
+ "parentTypeName": "Post",
+ },
+ "fieldName": "listLocations",
+ "arguments": {},
+ "source": {
+ "id": "1",
+ },
+ },
+ {
+ "typeName": "Query",
+ "info": {
+ "fieldName": "listLocations",
+ "parentTypeName": "Post",
+ },
+ "fieldName": "listLocations",
+ "arguments": {},
+ "source": {
+ "id": "2",
+ },
+ },
+ {
+ "typeName": "Query",
+ "info": {
+ "fieldName": "listLocations",
+ "parentTypeName": "Post",
+ },
+ "fieldName": "listLocations",
+ "arguments": {},
+ "source": {
+ "id": [3, 4],
+ },
+ },
+ ]
+
+ app = AppSyncResolver()
+
+ # WHEN the batch resolver for the listLocations field is defined
+ @app.batch_resolver(field_name="listLocations", aggregate=False)
+ def create_something(event: AppSyncResolverEvent) -> Optional[list]: # noqa AA03 VNE003
+ return event.source["id"] if event.source else None
+
+ # THEN the resolver should correctly process the batch of queries
+ result = app.resolve(event, LambdaContext())
+ assert result == [appsync_event["source"]["id"] for appsync_event in event]
+
+ assert app.current_batch_event and len(app.current_batch_event) == len(event)
+ assert not app.current_event
+
+
+def test_resolve_batch_processing_with_raise_on_exception_one_at_time():
+ # GIVEN a list of events representing GraphQL queries for listing locations
+ event = [
+ {
+ "typeName": "Query",
+ "info": {
+ "fieldName": "listLocations",
+ "parentTypeName": "Post",
+ },
+ "fieldName": "listLocations",
+ "arguments": {},
+ "source": {
+ "id": "1",
+ },
+ },
+ {
+ "typeName": "Query",
+ "info": {
+ "fieldName": "listLocations",
+ "parentTypeName": "Post",
+ },
+ "fieldName": "listLocations",
+ "arguments": {},
+ "source": {
+ "id": "2",
+ },
+ },
+ {
+ "typeName": "Query",
+ "info": {
+ "fieldName": "listLocations",
+ "parentTypeName": "Post",
+ },
+ "fieldName": "listLocations",
+ "arguments": {},
+ "source": {
+ "id": [3, 4],
+ },
+ },
+ ]
+
+ app = AppSyncResolver()
+
+ # WHEN the sync batch resolver for the 'listLocations' field is defined with raise_on_error=True
+ @app.batch_resolver(field_name="listLocations", raise_on_error=True, aggregate=False)
+ def create_something(event: AppSyncResolverEvent) -> Optional[list]: # noqa AA03 VNE003
+ raise RuntimeError
+
+ # THEN the resolver should raise a RuntimeError when processing the batch of queries
+ with pytest.raises(RuntimeError):
+ app.resolve(event, LambdaContext())
+
+
+def test_async_resolve_batch_processing_with_raise_on_exception_one_at_time():
+ # GIVEN a list of events representing GraphQL queries for listing locations
+ event = [
+ {
+ "typeName": "Query",
+ "info": {
+ "fieldName": "listLocations",
+ "parentTypeName": "Post",
+ },
+ "fieldName": "listLocations",
+ "arguments": {},
+ "source": {
+ "id": "1",
+ },
+ },
+ {
+ "typeName": "Query",
+ "info": {
+ "fieldName": "listLocations",
+ "parentTypeName": "Post",
+ },
+ "fieldName": "listLocations",
+ "arguments": {},
+ "source": {
+ "id": "2",
+ },
+ },
+ {
+ "typeName": "Query",
+ "info": {
+ "fieldName": "listLocations",
+ "parentTypeName": "Post",
+ },
+ "fieldName": "listLocations",
+ "arguments": {},
+ "source": {
+ "id": [3, 4],
+ },
+ },
+ ]
+
+ app = AppSyncResolver()
+
+ # WHEN the async batch resolver for the 'listLocations' field is defined with raise_on_error=True
+ @app.async_batch_resolver(field_name="listLocations", raise_on_error=True, aggregate=False)
+ async def create_something(event: AppSyncResolverEvent) -> Optional[list]: # noqa AA03 VNE003
+ raise RuntimeError
+
+ # THEN the resolver should raise a RuntimeError when processing the batch of queries
+ with pytest.raises(RuntimeError):
+ app.resolve(event, LambdaContext())
+
+
+def test_resolve_batch_processing_without_exception_one_at_time():
+ event = [
+ {
+ "typeName": "Query",
+ "info": {
+ "fieldName": "listLocations",
+ "parentTypeName": "Post",
+ },
+ "fieldName": "listLocations",
+ "arguments": {},
+ "source": {
+ "id": "1",
+ },
+ },
+ {
+ "typeName": "Query",
+ "info": {
+ "fieldName": "listLocations",
+ "parentTypeName": "Post",
+ },
+ "fieldName": "listLocations",
+ "arguments": {},
+ "source": {
+ "id": "2",
+ },
+ },
+ {
+ "typeName": "Query",
+ "info": {
+ "fieldName": "listLocations",
+ "parentTypeName": "Post",
+ },
+ "fieldName": "listLocations",
+ "arguments": {},
+ "source": {
+ "id": [3, 4],
+ },
+ },
+ ]
+
+ app = AppSyncResolver()
+
+ @app.batch_resolver(field_name="listLocations", raise_on_error=False, aggregate=False)
+ def create_something(event: AppSyncResolverEvent) -> Optional[list]: # noqa AA03 VNE003
+ raise RuntimeError
+
+ # Call the implicit handler
+ result = app.resolve(event, LambdaContext())
+ assert result == [None, None, None]
+
+ assert app.current_batch_event and len(app.current_batch_event) == len(event)
+ assert not app.current_event
+
+
+def test_resolve_async_batch_processing_without_exception_one_at_time():
+ # GIVEN a list of events representing GraphQL queries for listing locations
+ event = [
+ {
+ "typeName": "Query",
+ "info": {
+ "fieldName": "listLocations",
+ "parentTypeName": "Post",
+ },
+ "fieldName": "listLocations",
+ "arguments": {},
+ "source": {
+ "id": "1",
+ },
+ },
+ {
+ "typeName": "Query",
+ "info": {
+ "fieldName": "listLocations",
+ "parentTypeName": "Post",
+ },
+ "fieldName": "listLocations",
+ "arguments": {},
+ "source": {
+ "id": "2",
+ },
+ },
+ {
+ "typeName": "Query",
+ "info": {
+ "fieldName": "listLocations",
+ "parentTypeName": "Post",
+ },
+ "fieldName": "listLocations",
+ "arguments": {},
+ "source": {
+ "id": [3, 4],
+ },
+ },
+ ]
+
+ app = AppSyncResolver()
+
+ # WHEN the batch resolver for the 'listLocations' field is defined with raise_on_error=False
+ @app.async_batch_resolver(field_name="listLocations", raise_on_error=False, aggregate=False)
+ async def create_something(event: AppSyncResolverEvent) -> Optional[list]: # noqa AA03 VNE003
+ raise RuntimeError
+
+ result = app.resolve(event, LambdaContext())
+
+ # THEN the resolver should return None for each event in the batch
+ assert len(app.current_batch_event) == len(event)
+ assert result == [None, None, None]
+
+
+def test_resolver_batch_with_resolver_not_found_one_at_time():
+ # GIVEN a AppSyncResolver
+ app = AppSyncResolver()
+ router = Router()
+
+ # WHEN we have an event
+ # WHEN the event field_name doesn't match with the resolver field_name
+ mock_event1 = [
+ {
+ "typeName": "Query",
+ "info": {
+ "fieldName": "listCars",
+ "parentTypeName": "Query",
+ },
+ "fieldName": "listCars",
+ "arguments": {"name": "value"},
+ "source": {
+ "id": "1",
+ },
+ },
+ ]
+
+ @router.batch_resolver(type_name="Query", field_name="listLocations", aggregate=False)
+ def get_locations(event: AppSyncResolverEvent, name: str) -> str:
+ return f"get_locations#{name}#" + event.source["id"]
+
+ app.include_router(router)
+
+ # THEN must fail with ResolverNotFoundError
+ with pytest.raises(ResolverNotFoundError, match="No resolver found for.*"):
+ app.resolve(mock_event1, LambdaContext())
+
+
+def test_resolver_batch_with_sync_and_async_resolver_at_same_time():
+ # GIVEN a AppSyncResolver
+ app = AppSyncResolver()
+ router = Router()
+
+ # WHEN we have an event
+ # WHEN the event field_name doesn't match with the resolver field_name
+ mock_event1 = [
+ {
+ "typeName": "Query",
+ "info": {
+ "fieldName": "listCars",
+ "parentTypeName": "Query",
+ },
+ "fieldName": "listCars",
+ "arguments": {"name": "value"},
+ "source": {
+ "id": "1",
+ },
+ },
+ ]
+
+ @router.batch_resolver(type_name="Query", field_name="listCars", aggregate=False)
+ def get_locations(event: AppSyncResolverEvent, name: str) -> str:
+ return f"get_locations#{name}#" + event.source["id"]
+
+ @router.async_batch_resolver(type_name="Query", field_name="listCars", aggregate=False)
+ async def get_locations_async(event: AppSyncResolverEvent, name: str) -> str:
+ return f"get_locations#{name}#" + event.source["id"]
+
+ app.include_router(router)
+
+ # THEN must raise a PowertoolsUserWarning
+ with pytest.warns(PowertoolsUserWarning, match="Both synchronous and asynchronous resolvers*"):
+ app.resolve(mock_event1, LambdaContext())
+
+
+def test_batch_resolver_with_router():
+ # GIVEN an AppSyncResolver and a Router instance
+ app = AppSyncResolver()
+ router = Router()
+
+ @router.batch_resolver(type_name="Query", field_name="listLocations", aggregate=False)
+ def get_locations(event: AppSyncResolverEvent, name: str) -> str:
+ return f"get_locations#{name}#" + event.source["id"]
+
+ @router.batch_resolver(field_name="listLocations2", aggregate=False)
+ def get_locations2(event: AppSyncResolverEvent, name: str) -> str:
+ return f"get_locations2#{name}#" + event.source["id"]
+
+ # WHEN we include the routes
+ app.include_router(router)
+
+ mock_event1 = [
+ {
+ "typeName": "Query",
+ "info": {
+ "fieldName": "listLocations",
+ "parentTypeName": "Query",
+ },
+ "fieldName": "listLocations",
+ "arguments": {"name": "value"},
+ "source": {
+ "id": "1",
+ },
+ },
+ ]
+ mock_event2 = [
+ {
+ "typeName": "Query",
+ "info": {
+ "fieldName": "listLocations2",
+ "parentTypeName": "Post",
+ },
+ "fieldName": "listLocations2",
+ "arguments": {"name": "value"},
+ "source": {
+ "id": "2",
+ },
+ },
+ ]
+ result1 = app.resolve(mock_event1, LambdaContext())
+ result2 = app.resolve(mock_event2, LambdaContext())
+
+ # THEN the resolvers should return the expected results
+ assert result1 == ["get_locations#value#1"]
+ assert result2 == ["get_locations2#value#2"]
+
+
+def test_resolve_async_batch_processing():
+ # GIVEN a list of events representing GraphQL queries for listing locations
+ event = [
+ {
+ "typeName": "Query",
+ "info": {
+ "fieldName": "listLocations",
+ "parentTypeName": "Post",
+ },
+ "fieldName": "listLocations",
+ "arguments": {},
+ "source": {
+ "id": "1",
+ },
+ },
+ {
+ "typeName": "Query",
+ "info": {
+ "fieldName": "listLocations",
+ "parentTypeName": "Post",
+ },
+ "fieldName": "listLocations",
+ "arguments": {},
+ "source": {
+ "id": "2",
+ },
+ },
+ {
+ "typeName": "Query",
+ "info": {
+ "fieldName": "listLocations",
+ "parentTypeName": "Post",
+ },
+ "fieldName": "listLocations",
+ "arguments": {},
+ "source": {
+ "id": [3, 4],
+ },
+ },
+ ]
+
+ app = AppSyncResolver()
+
+ # WHEN the async batch resolver for the 'listLocations' field is defined
+ @app.async_batch_resolver(field_name="listLocations", aggregate=False)
+ async def create_something(event: AppSyncResolverEvent) -> Optional[list]:
+ return event.source["id"] if event.source else None
+
+ # THEN the resolver should correctly process the batch of queries asynchronously
+ result = app.resolve(event, LambdaContext())
+ assert result == [appsync_event["source"]["id"] for appsync_event in event]
+
+ assert app.current_batch_event and len(app.current_batch_event) == len(event)
+
+
+def test_resolve_async_batch_and_sync_singular_processing():
+ # GIVEN a router with an async batch resolver for 'listLocations' and a sync singular resolver for 'listLocation'
+ app = AppSyncResolver()
+ router = Router()
+
+ @router.async_batch_resolver(type_name="Query", field_name="listLocations", aggregate=False)
+ async def get_locations(event: AppSyncResolverEvent, name: str) -> str:
+ return f"get_locations#{name}#" + event.source["id"]
+
+ @app.resolver(type_name="Query", field_name="listLocation")
+ def get_location(name: str) -> str:
+ return f"get_location#{name}"
+
+ app.include_router(router)
+
+ # WHEN resolving a batch of events for async 'listLocations' and a singular event for 'listLocation'
+ mock_event1 = [
+ {
+ "typeName": "Query",
+ "info": {
+ "fieldName": "listLocations",
+ "parentTypeName": "Query",
+ },
+ "fieldName": "listLocations",
+ "arguments": {"name": "value"},
+ "source": {
+ "id": "1",
+ },
+ },
+ ]
+ mock_event2 = {"typeName": "Query", "fieldName": "listLocation", "arguments": {"name": "value"}}
+
+ result1 = app.resolve(mock_event1, LambdaContext())
+ result2 = app.resolve(mock_event2, LambdaContext())
+
+ # THEN the resolvers should return the expected results
+ assert result1 == ["get_locations#value#1"]
+ assert result2 == "get_location#value"
+
+
+def test_async_resolver_include_batch_resolver():
+ # GIVEN an AppSyncResolver instance and a Router
+ app = AppSyncResolver()
+ router = Router()
+
+ @router.async_batch_resolver(type_name="Query", field_name="listLocations", aggregate=False)
+ async def get_locations(event: AppSyncResolverEvent, name: str) -> str:
+ return f"get_locations#{name}#" + event.source["id"]
+
+ @app.async_batch_resolver(field_name="listLocations2", aggregate=False)
+ async def get_locations2(event: AppSyncResolverEvent, name: str) -> str:
+ return f"get_locations2#{name}#" + event.source["id"]
+
+ app.include_router(router)
+
+ # WHEN two different events needs to be resolved
+ mock_event1 = [
+ {
+ "typeName": "Query",
+ "info": {
+ "fieldName": "listLocations",
+ "parentTypeName": "Query",
+ },
+ "fieldName": "listLocations",
+ "arguments": {"name": "value"},
+ "source": {
+ "id": "1",
+ },
+ },
+ ]
+ mock_event2 = [
+ {
+ "typeName": "Query",
+ "info": {
+ "fieldName": "listLocations2",
+ "parentTypeName": "Post",
+ },
+ "fieldName": "listLocations2",
+ "arguments": {"name": "value"},
+ "source": {
+ "id": "2",
+ },
+ },
+ ]
+
+ # WHEN Resolve the events using the AppSyncResolver
+ result1 = app.resolve(mock_event1, LambdaContext())
+ result2 = app.resolve(mock_event2, LambdaContext())
+
+ # THEN Verify that the results match the expected values
+ assert result1 == ["get_locations#value#1"]
+ assert result2 == ["get_locations2#value#2"]
+
+
+def test_resolve_batch_processing_with_simple_queries_with_aggregate():
+ # GIVEN a list of events representing GraphQL queries for listing locations
+ event = [
+ {
+ "typeName": "Query",
+ "info": {
+ "fieldName": "listLocations",
+ "parentTypeName": "Post",
+ },
+ "fieldName": "listLocations",
+ "arguments": {},
+ "source": {
+ "id": "1",
+ },
+ },
+ {
+ "typeName": "Query",
+ "info": {
+ "fieldName": "listLocations",
+ "parentTypeName": "Post",
+ },
+ "fieldName": "listLocations",
+ "arguments": {},
+ "source": {
+ "id": "2",
+ },
+ },
+ {
+ "typeName": "Query",
+ "info": {
+ "fieldName": "listLocations",
+ "parentTypeName": "Post",
+ },
+ "fieldName": "listLocations",
+ "arguments": {},
+ "source": {
+ "id": [3, 4],
+ },
+ },
+ ]
+
+ app = AppSyncResolver()
+
+ # WHEN the sync batch resolver for the listLocations field is defined
+ # WHEN using an aggregated event
+ # WHEN function returns a List
+ @app.batch_resolver(field_name="listLocations")
+ def create_something(event: List[AppSyncResolverEvent]) -> List: # noqa AA03 VNE003
+ results = []
+ for record in event:
+ results.append(record.source.get("id") if record.source else None)
+
+ return results
+
+ # THEN the resolver should correctly process the batch of queries
+ result = app.resolve(event, LambdaContext())
+ assert result == [appsync_event["source"]["id"] for appsync_event in event]
+
+ assert app.current_batch_event and len(app.current_batch_event) == len(event)
+ assert not app.current_event
+
+
+def test_resolve_async_batch_processing_with_simple_queries_with_aggregate():
+ # GIVEN a list of events representing GraphQL queries for listing locations
+ event = [
+ {
+ "typeName": "Query",
+ "info": {
+ "fieldName": "listLocations",
+ "parentTypeName": "Post",
+ },
+ "fieldName": "listLocations",
+ "arguments": {},
+ "source": {
+ "id": "1",
+ },
+ },
+ {
+ "typeName": "Query",
+ "info": {
+ "fieldName": "listLocations",
+ "parentTypeName": "Post",
+ },
+ "fieldName": "listLocations",
+ "arguments": {},
+ "source": {
+ "id": "2",
+ },
+ },
+ {
+ "typeName": "Query",
+ "info": {
+ "fieldName": "listLocations",
+ "parentTypeName": "Post",
+ },
+ "fieldName": "listLocations",
+ "arguments": {},
+ "source": {
+ "id": [3, 4],
+ },
+ },
+ ]
+
+ app = AppSyncResolver()
+
+ # WHEN the async batch resolver for the listLocations field is defined
+ # WHEN using an aggregated event
+ # WHEN function returns a List
+ @app.async_batch_resolver(field_name="listLocations")
+ async def create_something(event: List[AppSyncResolverEvent]) -> List: # noqa AA03 VNE003
+ results = []
+ for record in event:
+ results.append(record.source.get("id") if record.source else None)
+
+ return results
+
+ # THEN the resolver should correctly process the batch of queries
+ result = app.resolve(event, LambdaContext())
+ assert result == [appsync_event["source"]["id"] for appsync_event in event]
+
+ assert app.current_batch_event and len(app.current_batch_event) == len(event)
+ assert not app.current_event
+
+
+def test_resolve_batch_processing_with_aggregate_and_returning_a_non_list():
+ # GIVEN a list of events representing GraphQL queries for listing locations
+ event = [
+ {
+ "typeName": "Query",
+ "info": {
+ "fieldName": "listLocations",
+ "parentTypeName": "Post",
+ },
+ "fieldName": "listLocations",
+ "arguments": {},
+ "source": {
+ "id": "1",
+ },
+ },
+ ]
+
+ app = AppSyncResolver()
+
+ # WHEN the sync batch resolver for the listLocations field is defined
+ # WHEN using an aggregated event
+ # WHEN function return something different than a List
+ @app.batch_resolver(field_name="listLocations")
+ def create_something(event: List[AppSyncResolverEvent]) -> Optional[List]: # noqa AA03 VNE003
+ return event[0].source.get("id") if event[0].source else None
+
+ # THEN the resolver should raise a InvalidBatchResponse when processing the batch of queries
+ with pytest.raises(InvalidBatchResponse):
+ app.resolve(event, LambdaContext())
+
+
+def test_resolve_async_batch_processing_with_aggregate_and_returning_a_non_list():
+ # GIVEN a list of events representing GraphQL queries for listing locations
+ event = [
+ {
+ "typeName": "Query",
+ "info": {
+ "fieldName": "listLocations",
+ "parentTypeName": "Post",
+ },
+ "fieldName": "listLocations",
+ "arguments": {},
+ "source": {
+ "id": "1",
+ },
+ },
+ ]
+
+ app = AppSyncResolver()
+
+ # WHEN the async batch resolver for the listLocations field is defined
+ # WHEN using an aggregated event
+ # WHEN function return something different than a List
+ @app.async_batch_resolver(field_name="listLocations")
+ async def create_something(event: List[AppSyncResolverEvent]) -> Optional[List]: # noqa AA03 VNE003
+ return event[0].source.get("id") if event[0].source else None
+
+ # THEN the resolver should raise a InvalidBatchResponse when processing the batch of queries
+ with pytest.raises(InvalidBatchResponse):
+ app.resolve(event, LambdaContext())
+
+
+def test_resolve_sync_batch_processing_with_aggregate_and_without_return():
+ # GIVEN a list of events representing GraphQL queries for listing locations
+ event = [
+ {
+ "typeName": "Query",
+ "info": {
+ "fieldName": "listLocations",
+ "parentTypeName": "Post",
+ },
+ "fieldName": "listLocations",
+ "arguments": {},
+ "source": {
+ "id": "1",
+ },
+ },
+ ]
+
+ app = AppSyncResolver()
+
+ # WHEN the sync batch resolver for the listLocations field is defined
+ # WHEN using an aggregated event
+ # WHEN function there is no return statement
+ @app.batch_resolver(field_name="listLocations")
+ def create_something(event: List[AppSyncResolverEvent]) -> Optional[List]: # noqa AA03 VNE003
+ def do_something_with_post_id(post_id): ...
+
+ post_id = event[0].source.get("id") if event[0].source else None
+ do_something_with_post_id(post_id)
+
+ # No Return statement
+
+ # THEN the resolver should raise a InvalidBatchResponse when processing the batch of queries
+ with pytest.raises(InvalidBatchResponse):
+ app.resolve(event, LambdaContext())
+
+
+def test_resolve_async_batch_processing_with_aggregate_and_without_return():
+ # GIVEN a list of events representing GraphQL queries for listing locations
+ event = [
+ {
+ "typeName": "Query",
+ "info": {
+ "fieldName": "listLocations",
+ "parentTypeName": "Post",
+ },
+ "fieldName": "listLocations",
+ "arguments": {},
+ "source": {
+ "id": "1",
+ },
+ },
+ ]
+
+ app = AppSyncResolver()
+
+ # WHEN the async batch resolver for the listLocations field is defined
+ # WHEN using an aggregated event
+ # WHEN function there is no return statement
+ @app.async_batch_resolver(field_name="listLocations")
+ async def create_something(event: List[AppSyncResolverEvent]) -> Optional[List]: # noqa AA03 VNE003
+ def do_something_with_post_id(post_id): ...
+
+ post_id = event[0].source.get("id") if event[0].source else None
+ do_something_with_post_id(post_id)
+
+ # No Return statement
+
+ # THEN the resolver should raise a InvalidBatchResponse when processing the batch of queries
+ with pytest.raises(InvalidBatchResponse):
+ app.resolve(event, LambdaContext())
diff --git a/tests/functional/event_handler/test_appsync.py b/tests/functional/event_handler/required_dependencies/appsync/test_appsync_single_resolvers.py
similarity index 97%
rename from tests/functional/event_handler/test_appsync.py
rename to tests/functional/event_handler/required_dependencies/appsync/test_appsync_single_resolvers.py
index 5699e560065..1ace266e731 100644
--- a/tests/functional/event_handler/test_appsync.py
+++ b/tests/functional/event_handler/required_dependencies/appsync/test_appsync_single_resolvers.py
@@ -3,7 +3,7 @@
import pytest
from aws_lambda_powertools.event_handler import AppSyncResolver
-from aws_lambda_powertools.event_handler.appsync import Router
+from aws_lambda_powertools.event_handler.graphql_appsync.router import Router
from aws_lambda_powertools.utilities.data_classes import AppSyncResolverEvent
from aws_lambda_powertools.utilities.typing import LambdaContext
from tests.functional.utils import load_event
@@ -169,11 +169,11 @@ def test_resolver_include_resolver():
@router.resolver(type_name="Query", field_name="listLocations")
def get_locations(name: str):
- return "get_locations#" + name
+ return f"get_locations#{name}"
@app.resolver(field_name="listLocations2")
def get_locations2(name: str):
- return "get_locations2#" + name
+ return f"get_locations2#{name}"
app.include_router(router)
@@ -225,7 +225,7 @@ def test_router_has_access_to_app_context():
@router.resolver(type_name="Query", field_name="listLocations")
def get_locations(name: str):
- if router.context["is_admin"]:
+ if router.context.get("is_admin"):
return f"get_locations#{name}"
app.include_router(router)
diff --git a/tests/functional/event_handler/required_dependencies/conftest.py b/tests/functional/event_handler/required_dependencies/conftest.py
new file mode 100644
index 00000000000..5c2bdb7729a
--- /dev/null
+++ b/tests/functional/event_handler/required_dependencies/conftest.py
@@ -0,0 +1,73 @@
+import json
+
+import pytest
+
+from tests.functional.utils import load_event
+
+
+@pytest.fixture
+def json_dump():
+ # our serializers reduce length to save on costs; fixture to replicate separators
+ return lambda obj: json.dumps(obj, separators=(",", ":"))
+
+
+@pytest.fixture
+def validation_schema():
+ return {
+ "$schema": "https://json-schema.org/draft-07/schema",
+ "$id": "https://example.com/example.json",
+ "type": "object",
+ "title": "Sample schema",
+ "description": "The root schema comprises the entire JSON document.",
+ "examples": [{"message": "hello world", "username": "lessa"}],
+ "required": ["message", "username"],
+ "properties": {
+ "message": {
+ "$id": "#/properties/message",
+ "type": "string",
+ "title": "The message",
+ "examples": ["hello world"],
+ },
+ "username": {
+ "$id": "#/properties/username",
+ "type": "string",
+ "title": "The username",
+ "examples": ["lessa"],
+ },
+ },
+ }
+
+
+@pytest.fixture
+def raw_event():
+ return {"message": "hello hello", "username": "blah blah"}
+
+
+@pytest.fixture
+def gw_event():
+ return load_event("apiGatewayProxyEvent.json")
+
+
+@pytest.fixture
+def gw_event_http():
+ return load_event("apiGatewayProxyV2Event.json")
+
+
+@pytest.fixture
+def gw_event_alb():
+ return load_event("albMultiValueQueryStringEvent.json")
+
+
+@pytest.fixture
+def gw_event_lambda_url():
+ return load_event("lambdaFunctionUrlEventWithHeaders.json")
+
+
+@pytest.fixture
+def gw_event_vpc_lattice():
+ return load_event("vpcLatticeV2EventWithHeaders.json")
+
+
+@pytest.fixture
+def gw_event_vpc_lattice_v1():
+ return load_event("vpcLatticeEvent.json")
diff --git a/tests/functional/event_handler/test_api_gateway.py b/tests/functional/event_handler/required_dependencies/test_api_gateway.py
similarity index 95%
rename from tests/functional/event_handler/test_api_gateway.py
rename to tests/functional/event_handler/required_dependencies/test_api_gateway.py
index 14af1ad175e..efd7edf0e5e 100644
--- a/tests/functional/event_handler/test_api_gateway.py
+++ b/tests/functional/event_handler/required_dependencies/test_api_gateway.py
@@ -10,7 +10,6 @@
from typing import Dict
import pytest
-from pydantic import BaseModel
from aws_lambda_powertools.event_handler import content_types
from aws_lambda_powertools.event_handler.api_gateway import (
@@ -31,7 +30,6 @@
ServiceError,
UnauthorizedError,
)
-from aws_lambda_powertools.event_handler.openapi.exceptions import RequestValidationError
from aws_lambda_powertools.shared import constants
from aws_lambda_powertools.shared.cookies import Cookie
from aws_lambda_powertools.shared.json_encoder import Encoder
@@ -45,7 +43,7 @@
def read_media(file_name: str) -> bytes:
- path = Path(str(Path(__file__).parent.parent.parent.parent) + "/docs/media/" + file_name)
+ path = Path(str(Path(__file__).parent.parent.parent.parent) + "/../docs/media/" + file_name)
return path.read_bytes()
@@ -1458,58 +1456,6 @@ def get_lambda() -> Response:
assert result["body"] == "Foo!"
-def test_exception_handler_with_data_validation():
- # GIVEN a resolver with an exception handler defined for RequestValidationError
- app = ApiGatewayResolver(enable_validation=True)
-
- @app.exception_handler(RequestValidationError)
- def handle_validation_error(ex: RequestValidationError):
- return Response(
- status_code=422,
- content_type=content_types.TEXT_PLAIN,
- body=f"Invalid data. Number of errors: {len(ex.errors())}",
- )
-
- @app.get("/my/path")
- def get_lambda(param: int): ...
-
- # WHEN calling the event handler
- # AND a RequestValidationError is raised
- result = app(LOAD_GW_EVENT, {})
-
- # THEN call the exception_handler
- assert result["statusCode"] == 422
- assert result["multiValueHeaders"]["Content-Type"] == [content_types.TEXT_PLAIN]
- assert result["body"] == "Invalid data. Number of errors: 1"
-
-
-def test_exception_handler_with_data_validation_pydantic_response():
- # GIVEN a resolver with an exception handler defined for RequestValidationError
- app = ApiGatewayResolver(enable_validation=True)
-
- class Err(BaseModel):
- msg: str
-
- @app.exception_handler(RequestValidationError)
- def handle_validation_error(ex: RequestValidationError):
- return Response(
- status_code=422,
- content_type=content_types.APPLICATION_JSON,
- body=Err(msg=f"Invalid data. Number of errors: {len(ex.errors())}"),
- )
-
- @app.get("/my/path")
- def get_lambda(param: int): ...
-
- # WHEN calling the event handler
- # AND a RequestValidationError is raised
- result = app(LOAD_GW_EVENT, {})
-
- # THEN exception handler's pydantic response should be serialized correctly
- assert result["statusCode"] == 422
- assert result["body"] == '{"msg":"Invalid data. Number of errors: 1"}'
-
-
def test_exception_handler_with_route():
app = ApiGatewayResolver()
# GIVEN a Router object with an exception handler defined for ValueError
@@ -1540,23 +1486,6 @@ def get_lambda() -> Response:
assert result["body"] == "Foo!"
-def test_data_validation_error():
- # GIVEN a resolver without an exception handler
- app = ApiGatewayResolver(enable_validation=True)
-
- @app.get("/my/path")
- def get_lambda(param: int): ...
-
- # WHEN calling the event handler
- # AND a RequestValidationError is raised
- result = app(LOAD_GW_EVENT, {})
-
- # THEN call the exception_handler
- assert result["statusCode"] == 422
- assert result["multiValueHeaders"]["Content-Type"] == [content_types.APPLICATION_JSON]
- assert "missing" in result["body"]
-
-
def test_exception_handler_service_error():
# GIVEN
app = ApiGatewayResolver()
diff --git a/tests/functional/event_handler/test_api_middlewares.py b/tests/functional/event_handler/required_dependencies/test_api_middlewares.py
similarity index 100%
rename from tests/functional/event_handler/test_api_middlewares.py
rename to tests/functional/event_handler/required_dependencies/test_api_middlewares.py
diff --git a/tests/functional/event_handler/test_base_path.py b/tests/functional/event_handler/required_dependencies/test_base_path.py
similarity index 86%
rename from tests/functional/event_handler/test_base_path.py
rename to tests/functional/event_handler/required_dependencies/test_base_path.py
index 479a46bda55..7fc5a0eced7 100644
--- a/tests/functional/event_handler/test_base_path.py
+++ b/tests/functional/event_handler/required_dependencies/test_base_path.py
@@ -10,7 +10,7 @@
def test_base_path_api_gateway_rest():
- app = APIGatewayRestResolver(enable_validation=True)
+ app = APIGatewayRestResolver()
@app.get("/")
def handle():
@@ -25,7 +25,7 @@ def handle():
def test_base_path_api_gateway_http():
- app = APIGatewayHttpResolver(enable_validation=True)
+ app = APIGatewayHttpResolver()
@app.get("/")
def handle():
@@ -42,7 +42,7 @@ def handle():
def test_base_path_alb():
- app = ALBResolver(enable_validation=True)
+ app = ALBResolver()
@app.get("/")
def handle():
@@ -57,7 +57,7 @@ def handle():
def test_base_path_lambda_function_url():
- app = LambdaFunctionUrlResolver(enable_validation=True)
+ app = LambdaFunctionUrlResolver()
@app.get("/")
def handle():
@@ -74,7 +74,7 @@ def handle():
def test_vpc_lattice():
- app = VPCLatticeResolver(enable_validation=True)
+ app = VPCLatticeResolver()
@app.get("/")
def handle():
@@ -89,7 +89,7 @@ def handle():
def test_vpc_latticev2():
- app = VPCLatticeV2Resolver(enable_validation=True)
+ app = VPCLatticeV2Resolver()
@app.get("/")
def handle():
diff --git a/tests/functional/event_handler/test_lambda_function_url.py b/tests/functional/event_handler/required_dependencies/test_lambda_function_url.py
similarity index 100%
rename from tests/functional/event_handler/test_lambda_function_url.py
rename to tests/functional/event_handler/required_dependencies/test_lambda_function_url.py
diff --git a/tests/functional/event_handler/test_router.py b/tests/functional/event_handler/required_dependencies/test_router.py
similarity index 100%
rename from tests/functional/event_handler/test_router.py
rename to tests/functional/event_handler/required_dependencies/test_router.py
diff --git a/tests/functional/event_handler/test_vpc_lattice.py b/tests/functional/event_handler/required_dependencies/test_vpc_lattice.py
similarity index 100%
rename from tests/functional/event_handler/test_vpc_lattice.py
rename to tests/functional/event_handler/required_dependencies/test_vpc_lattice.py
diff --git a/tests/functional/event_handler/test_vpc_latticev2.py b/tests/functional/event_handler/required_dependencies/test_vpc_latticev2.py
similarity index 100%
rename from tests/functional/event_handler/test_vpc_latticev2.py
rename to tests/functional/event_handler/required_dependencies/test_vpc_latticev2.py
diff --git a/tests/functional/feature_flags/_boto3/__init__.py b/tests/functional/feature_flags/_boto3/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/tests/functional/feature_flags/test_feature_flags.py b/tests/functional/feature_flags/_boto3/test_feature_flags.py
similarity index 100%
rename from tests/functional/feature_flags/test_feature_flags.py
rename to tests/functional/feature_flags/_boto3/test_feature_flags.py
diff --git a/tests/functional/feature_flags/test_schema_validation.py b/tests/functional/feature_flags/_boto3/test_schema_validation.py
similarity index 100%
rename from tests/functional/feature_flags/test_schema_validation.py
rename to tests/functional/feature_flags/_boto3/test_schema_validation.py
diff --git a/tests/functional/feature_flags/test_time_based_actions.py b/tests/functional/feature_flags/_boto3/test_time_based_actions.py
similarity index 100%
rename from tests/functional/feature_flags/test_time_based_actions.py
rename to tests/functional/feature_flags/_boto3/test_time_based_actions.py
diff --git a/tests/functional/idempotency/_boto3/__init__.py b/tests/functional/idempotency/_boto3/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/tests/functional/idempotency/conftest.py b/tests/functional/idempotency/_boto3/conftest.py
similarity index 100%
rename from tests/functional/idempotency/conftest.py
rename to tests/functional/idempotency/_boto3/conftest.py
diff --git a/tests/functional/idempotency/test_idempotency.py b/tests/functional/idempotency/_boto3/test_idempotency.py
similarity index 91%
rename from tests/functional/idempotency/test_idempotency.py
rename to tests/functional/idempotency/_boto3/test_idempotency.py
index 83ee16f328b..c0d1104faf3 100644
--- a/tests/functional/idempotency/test_idempotency.py
+++ b/tests/functional/idempotency/_boto3/test_idempotency.py
@@ -8,7 +8,6 @@
import pytest
from botocore import stub
from botocore.config import Config
-from pydantic import BaseModel
from pytest import FixtureRequest
from pytest_mock import MockerFixture
@@ -32,7 +31,6 @@
IdempotencyInconsistentStateError,
IdempotencyInvalidStatusError,
IdempotencyKeyError,
- IdempotencyModelTypeError,
IdempotencyNoSerializationModelError,
IdempotencyPersistenceLayerError,
IdempotencyValidationError,
@@ -47,9 +45,6 @@
from aws_lambda_powertools.utilities.idempotency.serialization.dataclass import (
DataclassSerializer,
)
-from aws_lambda_powertools.utilities.idempotency.serialization.pydantic import (
- PydanticSerializer,
-)
from aws_lambda_powertools.utilities.validation import envelopes, validator
from aws_lambda_powertools.warnings import PowertoolsUserWarning
from tests.functional.idempotency.utils import (
@@ -61,7 +56,7 @@
from tests.functional.utils import json_serialize, load_event
TABLE_NAME = "TEST_TABLE"
-TESTS_MODULE_PREFIX = "test-func.functional.idempotency.test_idempotency"
+TESTS_MODULE_PREFIX = "test-func.tests.functional.idempotency._boto3.test_idempotency"
def get_dataclasses_lib():
@@ -1315,106 +1310,6 @@ def record_handler(record):
assert from_dict_called is False, "in case response is None, from_dict should not be called"
-@pytest.mark.parametrize("output_serializer_type", ["explicit", "deduced"])
-def test_idempotent_function_serialization_pydantic(output_serializer_type: str):
- # GIVEN
- config = IdempotencyConfig(use_local_cache=True)
- mock_event = {"customer_id": "fake", "transaction_id": "fake-id"}
- idempotency_key = f"{TESTS_MODULE_PREFIX}.test_idempotent_function_serialization_pydantic..collect_payment#{hash_idempotency_key(mock_event)}" # noqa E501
- persistence_layer = MockPersistenceLayer(expected_idempotency_key=idempotency_key)
-
- class PaymentInput(BaseModel):
- customer_id: str
- transaction_id: str
-
- class PaymentOutput(BaseModel):
- customer_id: str
- transaction_id: str
-
- if output_serializer_type == "explicit":
- output_serializer = PydanticSerializer(
- model=PaymentOutput,
- )
- else:
- output_serializer = PydanticSerializer
-
- @idempotent_function(
- data_keyword_argument="payment",
- persistence_store=persistence_layer,
- config=config,
- output_serializer=output_serializer,
- )
- def collect_payment(payment: PaymentInput) -> PaymentOutput:
- return PaymentOutput(**payment.dict())
-
- # WHEN
- payment = PaymentInput(**mock_event)
- first_call: PaymentOutput = collect_payment(payment=payment)
- assert first_call.customer_id == payment.customer_id
- assert first_call.transaction_id == payment.transaction_id
- assert isinstance(first_call, PaymentOutput)
- second_call: PaymentOutput = collect_payment(payment=payment)
- assert isinstance(second_call, PaymentOutput)
- assert second_call.customer_id == payment.customer_id
- assert second_call.transaction_id == payment.transaction_id
-
-
-def test_idempotent_function_serialization_pydantic_failure_no_return_type():
- # GIVEN
- config = IdempotencyConfig(use_local_cache=True)
- mock_event = {"customer_id": "fake", "transaction_id": "fake-id"}
- idempotency_key = f"{TESTS_MODULE_PREFIX}.test_idempotent_function_serialization_pydantic_failure_no_return_type..collect_payment#{hash_idempotency_key(mock_event)}" # noqa E501
- persistence_layer = MockPersistenceLayer(expected_idempotency_key=idempotency_key)
-
- class PaymentInput(BaseModel):
- customer_id: str
- transaction_id: str
-
- class PaymentOutput(BaseModel):
- customer_id: str
- transaction_id: str
-
- idempotent_function_decorator = idempotent_function(
- data_keyword_argument="payment",
- persistence_store=persistence_layer,
- config=config,
- output_serializer=PydanticSerializer,
- )
- with pytest.raises(IdempotencyNoSerializationModelError, match="No serialization model was supplied"):
-
- @idempotent_function_decorator
- def collect_payment(payment: PaymentInput):
- return PaymentOutput(**payment.dict())
-
-
-def test_idempotent_function_serialization_pydantic_failure_bad_type():
- # GIVEN
- config = IdempotencyConfig(use_local_cache=True)
- mock_event = {"customer_id": "fake", "transaction_id": "fake-id"}
- idempotency_key = f"{TESTS_MODULE_PREFIX}.test_idempotent_function_serialization_pydantic_failure_no_return_type..collect_payment#{hash_idempotency_key(mock_event)}" # noqa E501
- persistence_layer = MockPersistenceLayer(expected_idempotency_key=idempotency_key)
-
- class PaymentInput(BaseModel):
- customer_id: str
- transaction_id: str
-
- class PaymentOutput(BaseModel):
- customer_id: str
- transaction_id: str
-
- idempotent_function_decorator = idempotent_function(
- data_keyword_argument="payment",
- persistence_store=persistence_layer,
- config=config,
- output_serializer=PydanticSerializer,
- )
- with pytest.raises(IdempotencyModelTypeError, match="Model type is not inherited from pydantic BaseModel"):
-
- @idempotent_function_decorator
- def collect_payment(payment: PaymentInput) -> dict:
- return PaymentOutput(**payment.dict())
-
-
@pytest.mark.parametrize("output_serializer_type", ["explicit", "deduced"])
def test_idempotent_function_serialization_dataclass(output_serializer_type: str):
# GIVEN
@@ -1493,37 +1388,6 @@ def collect_payment(payment: PaymentInput):
return PaymentOutput(**payment.dict())
-def test_idempotent_function_serialization_dataclass_failure_bad_type():
- # GIVEN
- dataclasses = get_dataclasses_lib()
- config = IdempotencyConfig(use_local_cache=True)
- mock_event = {"customer_id": "fake", "transaction_id": "fake-id"}
- idempotency_key = f"{TESTS_MODULE_PREFIX}.test_idempotent_function_serialization_pydantic_failure_no_return_type..collect_payment#{hash_idempotency_key(mock_event)}" # noqa E501
- persistence_layer = MockPersistenceLayer(expected_idempotency_key=idempotency_key)
-
- @dataclasses.dataclass
- class PaymentInput:
- customer_id: str
- transaction_id: str
-
- @dataclasses.dataclass
- class PaymentOutput:
- customer_id: str
- transaction_id: str
-
- idempotent_function_decorator = idempotent_function(
- data_keyword_argument="payment",
- persistence_store=persistence_layer,
- config=config,
- output_serializer=PydanticSerializer,
- )
- with pytest.raises(IdempotencyModelTypeError, match="Model type is not inherited from pydantic BaseModel"):
-
- @idempotent_function_decorator
- def collect_payment(payment: PaymentInput) -> dict:
- return PaymentOutput(**payment.dict())
-
-
def test_idempotent_function_arbitrary_args_kwargs():
# Scenario to validate we can use idempotent_function with a function
# with an arbitrary number of args and kwargs
@@ -1804,24 +1668,6 @@ class Foo:
assert as_dict == expected_result
-def test_idempotent_function_pydantic():
- # Scenario _prepare_data should convert a pydantic to a dict
- class Foo(BaseModel):
- name: str
-
- expected_result = {"name": "Bar"}
- data = Foo(name="Bar")
- as_dict = _prepare_data(data)
- assert as_dict == data.dict()
- assert as_dict == expected_result
-
-
-@pytest.mark.parametrize("data", [None, "foo", ["foo"], 1, True, {}])
-def test_idempotent_function_other(data):
- # All other data types should be left as is
- assert _prepare_data(data) == data
-
-
def test_idempotent_function_dataclass_with_jmespath():
# GIVEN
dataclasses = get_dataclasses_lib()
@@ -1847,29 +1693,6 @@ def collect_payment(payment: Payment):
assert result == payment.transaction_id
-def test_idempotent_function_pydantic_with_jmespath():
- # GIVEN
- config = IdempotencyConfig(event_key_jmespath="transaction_id", use_local_cache=True)
- mock_event = {"customer_id": "fake", "transaction_id": "fake-id"}
- idempotency_key = f"{TESTS_MODULE_PREFIX}.test_idempotent_function_pydantic_with_jmespath..collect_payment#{hash_idempotency_key(mock_event['transaction_id'])}" # noqa E501
- persistence_layer = MockPersistenceLayer(expected_idempotency_key=idempotency_key)
-
- class Payment(BaseModel):
- customer_id: str
- transaction_id: str
-
- @idempotent_function(data_keyword_argument="payment", persistence_store=persistence_layer, config=config)
- def collect_payment(payment: Payment):
- return payment.transaction_id
-
- # WHEN
- payment = Payment(**mock_event)
- result = collect_payment(payment=payment)
-
- # THEN idempotency key assertion happens at MockPersistenceLayer
- assert result == payment.transaction_id
-
-
@pytest.mark.parametrize("idempotency_config", [{"use_local_cache": False}], indirect=True)
def test_idempotent_lambda_compound_already_completed(
idempotency_config: IdempotencyConfig,
diff --git a/tests/functional/idempotency/_pydantic/__init__.py b/tests/functional/idempotency/_pydantic/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/tests/functional/idempotency/_pydantic/test_idempotency_with_pydantic.py b/tests/functional/idempotency/_pydantic/test_idempotency_with_pydantic.py
new file mode 100644
index 00000000000..e5e488e6f49
--- /dev/null
+++ b/tests/functional/idempotency/_pydantic/test_idempotency_with_pydantic.py
@@ -0,0 +1,221 @@
+import pytest
+from pydantic import BaseModel
+
+from aws_lambda_powertools.utilities.idempotency import (
+ IdempotencyConfig,
+ idempotent_function,
+)
+from aws_lambda_powertools.utilities.idempotency.base import (
+ _prepare_data,
+)
+from aws_lambda_powertools.utilities.idempotency.exceptions import (
+ IdempotencyModelTypeError,
+ IdempotencyNoSerializationModelError,
+)
+from aws_lambda_powertools.utilities.idempotency.persistence.base import (
+ BasePersistenceLayer,
+ DataRecord,
+)
+from aws_lambda_powertools.utilities.idempotency.serialization.pydantic import (
+ PydanticSerializer,
+)
+from tests.functional.idempotency.utils import (
+ hash_idempotency_key,
+)
+
+TESTS_MODULE_PREFIX = "test-func.tests.functional.idempotency._pydantic.test_idempotency_with_pydantic"
+
+
+def get_dataclasses_lib():
+ """Python 3.6 doesn't support dataclasses natively"""
+ import dataclasses
+
+ return dataclasses
+
+
+class MockPersistenceLayer(BasePersistenceLayer):
+ def __init__(self, expected_idempotency_key: str):
+ self.expected_idempotency_key = expected_idempotency_key
+ super(MockPersistenceLayer, self).__init__()
+
+ def _put_record(self, data_record: DataRecord) -> None:
+ assert data_record.idempotency_key == self.expected_idempotency_key
+
+ def _update_record(self, data_record: DataRecord) -> None:
+ assert data_record.idempotency_key == self.expected_idempotency_key
+
+ def _get_record(self, idempotency_key) -> DataRecord: ...
+
+ def _delete_record(self, data_record: DataRecord) -> None: ...
+
+
+@pytest.mark.parametrize("output_serializer_type", ["explicit", "deduced"])
+def test_idempotent_function_serialization_pydantic(output_serializer_type: str):
+ # GIVEN
+ config = IdempotencyConfig(use_local_cache=True)
+ mock_event = {"customer_id": "fake", "transaction_id": "fake-id"}
+ idempotency_key = f"{TESTS_MODULE_PREFIX}.test_idempotent_function_serialization_pydantic..collect_payment#{hash_idempotency_key(mock_event)}" # noqa E501
+ persistence_layer = MockPersistenceLayer(expected_idempotency_key=idempotency_key)
+
+ class PaymentInput(BaseModel):
+ customer_id: str
+ transaction_id: str
+
+ class PaymentOutput(BaseModel):
+ customer_id: str
+ transaction_id: str
+
+ if output_serializer_type == "explicit":
+ output_serializer = PydanticSerializer(
+ model=PaymentOutput,
+ )
+ else:
+ output_serializer = PydanticSerializer
+
+ @idempotent_function(
+ data_keyword_argument="payment",
+ persistence_store=persistence_layer,
+ config=config,
+ output_serializer=output_serializer,
+ )
+ def collect_payment(payment: PaymentInput) -> PaymentOutput:
+ return PaymentOutput(**payment.dict())
+
+ # WHEN
+ payment = PaymentInput(**mock_event)
+ first_call: PaymentOutput = collect_payment(payment=payment)
+ assert first_call.customer_id == payment.customer_id
+ assert first_call.transaction_id == payment.transaction_id
+ assert isinstance(first_call, PaymentOutput)
+ second_call: PaymentOutput = collect_payment(payment=payment)
+ assert isinstance(second_call, PaymentOutput)
+ assert second_call.customer_id == payment.customer_id
+ assert second_call.transaction_id == payment.transaction_id
+
+
+def test_idempotent_function_serialization_pydantic_failure_no_return_type():
+ # GIVEN
+ config = IdempotencyConfig(use_local_cache=True)
+ mock_event = {"customer_id": "fake", "transaction_id": "fake-id"}
+ idempotency_key = f"{TESTS_MODULE_PREFIX}.test_idempotent_function_serialization_pydantic_failure_no_return_type..collect_payment#{hash_idempotency_key(mock_event)}" # noqa E501
+ persistence_layer = MockPersistenceLayer(expected_idempotency_key=idempotency_key)
+
+ class PaymentInput(BaseModel):
+ customer_id: str
+ transaction_id: str
+
+ class PaymentOutput(BaseModel):
+ customer_id: str
+ transaction_id: str
+
+ idempotent_function_decorator = idempotent_function(
+ data_keyword_argument="payment",
+ persistence_store=persistence_layer,
+ config=config,
+ output_serializer=PydanticSerializer,
+ )
+ with pytest.raises(IdempotencyNoSerializationModelError, match="No serialization model was supplied"):
+
+ @idempotent_function_decorator
+ def collect_payment(payment: PaymentInput):
+ return PaymentOutput(**payment.dict())
+
+
+def test_idempotent_function_serialization_pydantic_failure_bad_type():
+ # GIVEN
+ config = IdempotencyConfig(use_local_cache=True)
+ mock_event = {"customer_id": "fake", "transaction_id": "fake-id"}
+ idempotency_key = f"{TESTS_MODULE_PREFIX}.test_idempotent_function_serialization_pydantic_failure_no_return_type..collect_payment#{hash_idempotency_key(mock_event)}" # noqa E501
+ persistence_layer = MockPersistenceLayer(expected_idempotency_key=idempotency_key)
+
+ class PaymentInput(BaseModel):
+ customer_id: str
+ transaction_id: str
+
+ class PaymentOutput(BaseModel):
+ customer_id: str
+ transaction_id: str
+
+ idempotent_function_decorator = idempotent_function(
+ data_keyword_argument="payment",
+ persistence_store=persistence_layer,
+ config=config,
+ output_serializer=PydanticSerializer,
+ )
+ with pytest.raises(IdempotencyModelTypeError, match="Model type is not inherited from pydantic BaseModel"):
+
+ @idempotent_function_decorator
+ def collect_payment(payment: PaymentInput) -> dict:
+ return PaymentOutput(**payment.dict())
+
+
+def test_idempotent_function_serialization_dataclass_failure_bad_type():
+ # GIVEN
+ dataclasses = get_dataclasses_lib()
+ config = IdempotencyConfig(use_local_cache=True)
+ mock_event = {"customer_id": "fake", "transaction_id": "fake-id"}
+ idempotency_key = f"{TESTS_MODULE_PREFIX}.test_idempotent_function_serialization_pydantic_failure_no_return_type..collect_payment#{hash_idempotency_key(mock_event)}" # noqa E501
+ persistence_layer = MockPersistenceLayer(expected_idempotency_key=idempotency_key)
+
+ @dataclasses.dataclass
+ class PaymentInput:
+ customer_id: str
+ transaction_id: str
+
+ @dataclasses.dataclass
+ class PaymentOutput:
+ customer_id: str
+ transaction_id: str
+
+ idempotent_function_decorator = idempotent_function(
+ data_keyword_argument="payment",
+ persistence_store=persistence_layer,
+ config=config,
+ output_serializer=PydanticSerializer,
+ )
+ with pytest.raises(IdempotencyModelTypeError, match="Model type is not inherited from pydantic BaseModel"):
+
+ @idempotent_function_decorator
+ def collect_payment(payment: PaymentInput) -> dict:
+ return PaymentOutput(**payment.dict())
+
+
+def test_idempotent_function_pydantic():
+ # Scenario _prepare_data should convert a pydantic to a dict
+ class Foo(BaseModel):
+ name: str
+
+ expected_result = {"name": "Bar"}
+ data = Foo(name="Bar")
+ as_dict = _prepare_data(data)
+ assert as_dict == data.dict()
+ assert as_dict == expected_result
+
+
+@pytest.mark.parametrize("data", [None, "foo", ["foo"], 1, True, {}])
+def test_idempotent_function_other(data):
+ # All other data types should be left as is
+ assert _prepare_data(data) == data
+
+
+def test_idempotent_function_pydantic_with_jmespath():
+ # GIVEN
+ config = IdempotencyConfig(event_key_jmespath="transaction_id", use_local_cache=True)
+ mock_event = {"customer_id": "fake", "transaction_id": "fake-id"}
+ idempotency_key = f"{TESTS_MODULE_PREFIX}.test_idempotent_function_pydantic_with_jmespath..collect_payment#{hash_idempotency_key(mock_event['transaction_id'])}" # noqa E501
+ persistence_layer = MockPersistenceLayer(expected_idempotency_key=idempotency_key)
+
+ class Payment(BaseModel):
+ customer_id: str
+ transaction_id: str
+
+ @idempotent_function(data_keyword_argument="payment", persistence_store=persistence_layer, config=config)
+ def collect_payment(payment: Payment):
+ return payment.transaction_id
+
+ # WHEN
+ payment = Payment(**mock_event)
+ result = collect_payment(payment=payment)
+
+ # THEN idempotency key assertion happens at MockPersistenceLayer
+ assert result == payment.transaction_id
diff --git a/tests/functional/idempotency/_redis/__init__.py b/tests/functional/idempotency/_redis/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/tests/functional/idempotency/persistence/test_redis_layer.py b/tests/functional/idempotency/_redis/test_redis_layer.py
similarity index 100%
rename from tests/functional/idempotency/persistence/test_redis_layer.py
rename to tests/functional/idempotency/_redis/test_redis_layer.py
diff --git a/tests/functional/idempotency/utils.py b/tests/functional/idempotency/utils.py
index c396e40957c..1bf14393990 100644
--- a/tests/functional/idempotency/utils.py
+++ b/tests/functional/idempotency/utils.py
@@ -19,7 +19,7 @@ def build_idempotency_put_item_stub(
data: Dict,
function_name: str = "test-func",
function_qualified_name: str = "test_idempotent_lambda_first_execution_event_mutation.",
- module_name: str = "functional.idempotency.test_idempotency",
+ module_name: str = "tests.functional.idempotency._boto3.test_idempotency",
handler_name: str = "lambda_handler",
) -> Dict:
idempotency_key_hash = (
@@ -57,7 +57,7 @@ def build_idempotency_update_item_stub(
handler_response: Dict,
function_name: str = "test-func",
function_qualified_name: str = "test_idempotent_lambda_first_execution_event_mutation.",
- module_name: str = "functional.idempotency.test_idempotency",
+ module_name: str = "tests.functional.idempotency._boto3.test_idempotency",
handler_name: str = "lambda_handler",
) -> Dict:
idempotency_key_hash = (
diff --git a/tests/functional/logger/__init__.py b/tests/functional/logger/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/tests/functional/logger/required_dependencies/__init__.py b/tests/functional/logger/required_dependencies/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/tests/functional/test_logger.py b/tests/functional/logger/required_dependencies/test_logger.py
similarity index 94%
rename from tests/functional/test_logger.py
rename to tests/functional/logger/required_dependencies/test_logger.py
index 7aa4037cb9c..e86dba27eb6 100644
--- a/tests/functional/test_logger.py
+++ b/tests/functional/logger/required_dependencies/test_logger.py
@@ -8,21 +8,19 @@
import secrets
import string
import sys
-import warnings
from collections import namedtuple
from datetime import datetime, timezone
from typing import Any, Callable, Dict, Iterable, List, Optional, Union
import pytest
-from aws_lambda_powertools import Logger, Tracer, set_package_logger_handler
+from aws_lambda_powertools import Logger
from aws_lambda_powertools.logging import correlation_paths
from aws_lambda_powertools.logging.exceptions import InvalidLoggerSamplingRateError
from aws_lambda_powertools.logging.formatter import (
BasePowertoolsFormatter,
LambdaPowertoolsFormatter,
)
-from aws_lambda_powertools.logging.logger import set_package_logger
from aws_lambda_powertools.shared import constants
from aws_lambda_powertools.utilities.data_classes import S3Event, event_source
@@ -215,36 +213,6 @@ def handler(event, context):
assert second_log["cold_start"] is False
-def test_package_logger_stream(stdout):
- # GIVEN package logger "aws_lambda_powertools" is explicitly set with no params
- set_package_logger(stream=stdout)
-
- # WHEN Tracer is initialized in disabled mode
- Tracer(disabled=True)
-
- # THEN Tracer debug log statement should be logged
- output = stdout.getvalue()
- logger = logging.getLogger("aws_lambda_powertools")
- assert "Tracing has been disabled" in output
- assert logger.level == logging.DEBUG
-
-
-def test_package_logger_format(capsys):
- # GIVEN package logger "aws_lambda_powertools" is explicitly
- # with a custom formatter
- formatter = logging.Formatter("message=%(message)s")
- set_package_logger(formatter=formatter)
-
- # WHEN Tracer is initialized in disabled mode
- Tracer(disabled=True)
-
- # THEN Tracer debug log statement should be logged using `message=` format
- output = capsys.readouterr().out
- logger = logging.getLogger("aws_lambda_powertools")
- assert "message=" in output
- assert logger.level == logging.DEBUG
-
-
def test_logger_append_duplicated(stdout, service_name):
# GIVEN Logger is initialized with request_id field
logger = Logger(service=service_name, stream=stdout, request_id="value")
@@ -971,36 +939,6 @@ def handler(event, context, planet, str_end="."):
assert log["message"] == "Hello World!"
-def test_set_package_logger_handler_with_powertools_debug_env_var(stdout, monkeypatch: pytest.MonkeyPatch):
- # GIVEN POWERTOOLS_DEBUG is set
- monkeypatch.setenv(constants.POWERTOOLS_DEBUG_ENV, "1")
- logger = logging.getLogger("aws_lambda_powertools")
-
- # WHEN set_package_logger is used at initialization
- # and any Powertools for AWS Lambda (Python) operation is used (e.g., Tracer)
- set_package_logger_handler(stream=stdout)
- Tracer(disabled=True)
-
- # THEN Tracer debug log statement should be logged
- output = stdout.getvalue()
- assert "Tracing has been disabled" in output
- assert logger.level == logging.DEBUG
-
-
-def test_powertools_debug_env_var_warning(monkeypatch: pytest.MonkeyPatch):
- # GIVEN POWERTOOLS_DEBUG is set
- monkeypatch.setenv(constants.POWERTOOLS_DEBUG_ENV, "1")
- warning_message = "POWERTOOLS_DEBUG environment variable is enabled. Setting logging level to DEBUG."
-
- # WHEN set_package_logger is used at initialization
- # THEN a warning should be emitted
- with warnings.catch_warnings(record=True) as w:
- warnings.simplefilter("default")
- set_package_logger_handler()
- assert len(w) == 1
- assert str(w[0].message) == warning_message
-
-
def test_logger_log_uncaught_exceptions(service_name, stdout):
# GIVEN an initialized Logger is set with log_uncaught_exceptions
logger = Logger(service=service_name, stream=stdout, log_uncaught_exceptions=True)
diff --git a/tests/functional/test_logger_powertools_formatter.py b/tests/functional/logger/required_dependencies/test_logger_powertools_formatter.py
similarity index 100%
rename from tests/functional/test_logger_powertools_formatter.py
rename to tests/functional/logger/required_dependencies/test_logger_powertools_formatter.py
diff --git a/tests/functional/test_logger_utils.py b/tests/functional/logger/required_dependencies/test_logger_utils.py
similarity index 100%
rename from tests/functional/test_logger_utils.py
rename to tests/functional/logger/required_dependencies/test_logger_utils.py
diff --git a/tests/functional/logger/required_dependencies/test_logger_with_package_logger.py b/tests/functional/logger/required_dependencies/test_logger_with_package_logger.py
new file mode 100644
index 00000000000..2dfd6016333
--- /dev/null
+++ b/tests/functional/logger/required_dependencies/test_logger_with_package_logger.py
@@ -0,0 +1,113 @@
+import io
+import json
+import logging
+import random
+import string
+import warnings
+from collections import namedtuple
+
+import pytest
+
+from aws_lambda_powertools import Metrics, set_package_logger_handler
+from aws_lambda_powertools.logging.logger import set_package_logger
+from aws_lambda_powertools.shared import constants
+
+
+@pytest.fixture
+def stdout():
+ return io.StringIO()
+
+
+@pytest.fixture
+def lambda_context():
+ lambda_context = {
+ "function_name": "test",
+ "memory_limit_in_mb": 128,
+ "invoked_function_arn": "arn:aws:lambda:eu-west-1:809313241:function:test",
+ "aws_request_id": "52fdfc07-2182-154f-163f-5f0f9a621d72",
+ }
+
+ return namedtuple("LambdaContext", lambda_context.keys())(*lambda_context.values())
+
+
+@pytest.fixture
+def lambda_event():
+ return {"greeting": "hello"}
+
+
+@pytest.fixture
+def service_name():
+ chars = string.ascii_letters + string.digits
+ return "".join(random.SystemRandom().choice(chars) for _ in range(15))
+
+
+def capture_logging_output(stdout):
+ return json.loads(stdout.getvalue().strip())
+
+
+def capture_multiple_logging_statements_output(stdout):
+ return [json.loads(line.strip()) for line in stdout.getvalue().split("\n") if line]
+
+
+def test_package_logger_stream(stdout):
+ # GIVEN package logger "aws_lambda_powertools" is explicitly set with no params
+ set_package_logger(stream=stdout)
+
+ # WHEN we add a dimension in Metrics feature
+ my_metrics = Metrics(namespace="powertools")
+ my_metrics.add_dimension(name="dimension", value="test")
+
+ # THEN Metrics debug log statement should be logged
+ output = stdout.getvalue()
+ logger = logging.getLogger("aws_lambda_powertools")
+ assert "Adding dimension:" in output
+ assert logger.level == logging.DEBUG
+
+
+def test_package_logger_format(capsys):
+ # GIVEN package logger "aws_lambda_powertools" is explicitly
+ # with a custom formatter
+ formatter = logging.Formatter("message=%(message)s")
+ set_package_logger(formatter=formatter)
+
+ # WHEN we add a dimension in Metrics feature
+ my_metrics = Metrics(namespace="powertools")
+ my_metrics.add_dimension(name="dimension", value="test")
+
+ # THEN Metrics debug log statement should be logged using `message=` format
+ output = capsys.readouterr().out
+ logger = logging.getLogger("aws_lambda_powertools")
+ assert "message=" in output
+ assert logger.level == logging.DEBUG
+
+
+def test_set_package_logger_handler_with_powertools_debug_env_var(stdout, monkeypatch: pytest.MonkeyPatch):
+ # GIVEN POWERTOOLS_DEBUG is set
+ monkeypatch.setenv(constants.POWERTOOLS_DEBUG_ENV, "1")
+ logger = logging.getLogger("aws_lambda_powertools")
+
+ # WHEN set_package_logger is used at initialization
+ # and any Powertools for AWS Lambda (Python) operation is used (e.g., Metrics add_dimension)
+ set_package_logger_handler(stream=stdout)
+
+ my_metrics = Metrics(namespace="powertools")
+ my_metrics.add_dimension(name="dimension", value="test")
+
+ # THEN Metrics debug log statement should be logged
+ output = stdout.getvalue()
+ assert "Adding dimension:" in output
+ assert logger.level == logging.DEBUG
+
+
+def test_powertools_debug_env_var_warning(monkeypatch: pytest.MonkeyPatch):
+ # GIVEN POWERTOOLS_DEBUG is set
+ monkeypatch.setenv(constants.POWERTOOLS_DEBUG_ENV, "1")
+ warning_message = "POWERTOOLS_DEBUG environment variable is enabled. Setting logging level to DEBUG."
+
+ # WHEN set_package_logger is used at initialization
+ # THEN a warning should be emitted
+ with warnings.catch_warnings(record=True) as w:
+ warnings.simplefilter("default")
+ set_package_logger_handler()
+ assert len(w) == 1
+ assert str(w[0].message) == warning_message
diff --git a/tests/functional/metrics/__init__.py b/tests/functional/metrics/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/tests/functional/metrics/datadog/__init__.py b/tests/functional/metrics/datadog/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/tests/functional/metrics/test_metrics_datadog.py b/tests/functional/metrics/datadog/test_metrics_datadog.py
similarity index 99%
rename from tests/functional/metrics/test_metrics_datadog.py
rename to tests/functional/metrics/datadog/test_metrics_datadog.py
index abedfd99424..2626b8755c6 100644
--- a/tests/functional/metrics/test_metrics_datadog.py
+++ b/tests/functional/metrics/datadog/test_metrics_datadog.py
@@ -3,7 +3,6 @@
from collections import namedtuple
import pytest
-from test_metrics_provider import capture_metrics_output
from aws_lambda_powertools.metrics.exceptions import MetricValueError, SchemaValidationError
from aws_lambda_powertools.metrics.provider.cold_start import reset_cold_start_flag
@@ -40,7 +39,7 @@ def test_datadog_write_to_log_with_env_variable(capsys, monkeypatch):
# WHEN we add a metric
metrics.add_metric(name="item_sold", value=1, product="latte", order="online")
metrics.flush_metrics()
- logs = capture_metrics_output(capsys)
+ logs = json.loads(capsys.readouterr().out.strip())
# THEN metrics is flushed to log
logs["e"] = ""
diff --git a/tests/functional/metrics/required_dependencies/__init__.py b/tests/functional/metrics/required_dependencies/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/tests/functional/metrics/test_metrics_cloudwatch_emf.py b/tests/functional/metrics/required_dependencies/test_metrics_cloudwatch_emf.py
similarity index 100%
rename from tests/functional/metrics/test_metrics_cloudwatch_emf.py
rename to tests/functional/metrics/required_dependencies/test_metrics_cloudwatch_emf.py
diff --git a/tests/functional/metrics/test_metrics_provider.py b/tests/functional/metrics/required_dependencies/test_metrics_provider.py
similarity index 100%
rename from tests/functional/metrics/test_metrics_provider.py
rename to tests/functional/metrics/required_dependencies/test_metrics_provider.py
diff --git a/tests/functional/middleware_factory/_aws_xray_sdk/__init__.py b/tests/functional/middleware_factory/_aws_xray_sdk/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/tests/functional/middleware_factory/_aws_xray_sdk/test_middleware_factory_tracing.py b/tests/functional/middleware_factory/_aws_xray_sdk/test_middleware_factory_tracing.py
new file mode 100644
index 00000000000..0e19ac39aa3
--- /dev/null
+++ b/tests/functional/middleware_factory/_aws_xray_sdk/test_middleware_factory_tracing.py
@@ -0,0 +1,32 @@
+from aws_lambda_powertools.middleware_factory import lambda_handler_decorator
+
+
+def test_factory_explicit_tracing(monkeypatch):
+ monkeypatch.setenv("POWERTOOLS_TRACE_DISABLED", "true")
+
+ @lambda_handler_decorator(trace_execution=True)
+ def no_op(handler, event, context):
+ ret = handler(event, context)
+ return ret
+
+ @no_op
+ def lambda_handler(evt, ctx):
+ return True
+
+ lambda_handler({}, {})
+
+
+def test_factory_explicit_tracing_env_var(monkeypatch):
+ monkeypatch.setenv("POWERTOOLS_TRACE_MIDDLEWARES", "true")
+ monkeypatch.setenv("POWERTOOLS_TRACE_DISABLED", "true")
+
+ @lambda_handler_decorator
+ def no_op(handler, event, context):
+ ret = handler(event, context)
+ return ret
+
+ @no_op
+ def lambda_handler(evt, ctx):
+ return True
+
+ lambda_handler({}, {})
diff --git a/tests/functional/middleware_factory/required_dependencies/__init__.py b/tests/functional/middleware_factory/required_dependencies/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/tests/functional/test_middleware_factory.py b/tests/functional/middleware_factory/required_dependencies/test_middleware_factory.py
similarity index 80%
rename from tests/functional/test_middleware_factory.py
rename to tests/functional/middleware_factory/required_dependencies/test_middleware_factory.py
index fb868cef0ee..7481e2b8f6b 100644
--- a/tests/functional/test_middleware_factory.py
+++ b/tests/functional/middleware_factory/required_dependencies/test_middleware_factory.py
@@ -62,37 +62,6 @@ def lambda_handler(evt, ctx):
lambda_handler({}, {})
-def test_factory_explicit_tracing(monkeypatch):
- monkeypatch.setenv("POWERTOOLS_TRACE_DISABLED", "true")
-
- @lambda_handler_decorator(trace_execution=True)
- def no_op(handler, event, context):
- ret = handler(event, context)
- return ret
-
- @no_op
- def lambda_handler(evt, ctx):
- return True
-
- lambda_handler({}, {})
-
-
-def test_factory_explicit_tracing_env_var(monkeypatch):
- monkeypatch.setenv("POWERTOOLS_TRACE_MIDDLEWARES", "true")
- monkeypatch.setenv("POWERTOOLS_TRACE_DISABLED", "true")
-
- @lambda_handler_decorator
- def no_op(handler, event, context):
- ret = handler(event, context)
- return ret
-
- @no_op
- def lambda_handler(evt, ctx):
- return True
-
- lambda_handler({}, {})
-
-
def test_factory_decorator_with_kwarg_params(capsys):
@lambda_handler_decorator
def log_event(handler, event, context, log_event=False):
diff --git a/tests/functional/parameters/__init__.py b/tests/functional/parameters/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/tests/functional/test_utilities_parameters.py b/tests/functional/parameters/_boto3/test_utilities_parameters.py
similarity index 100%
rename from tests/functional/test_utilities_parameters.py
rename to tests/functional/parameters/_boto3/test_utilities_parameters.py
diff --git a/tests/functional/streaming/_boto3/__init__.py b/tests/functional/streaming/_boto3/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/tests/functional/streaming/test_s3_object.py b/tests/functional/streaming/_boto3/test_s3_object.py
similarity index 100%
rename from tests/functional/streaming/test_s3_object.py
rename to tests/functional/streaming/_boto3/test_s3_object.py
diff --git a/tests/functional/streaming/test_s3_seekable_io.py b/tests/functional/streaming/_boto3/test_s3_seekable_io.py
similarity index 100%
rename from tests/functional/streaming/test_s3_seekable_io.py
rename to tests/functional/streaming/_boto3/test_s3_seekable_io.py
diff --git a/tests/functional/tracer/__init__.py b/tests/functional/tracer/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/tests/functional/tracer/_aws_xray_sdk/__init__.py b/tests/functional/tracer/_aws_xray_sdk/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/tests/functional/test_tracing.py b/tests/functional/tracer/_aws_xray_sdk/test_tracing.py
similarity index 100%
rename from tests/functional/test_tracing.py
rename to tests/functional/tracer/_aws_xray_sdk/test_tracing.py
diff --git a/tests/functional/typing/__init__.py b/tests/functional/typing/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/tests/functional/typing/required_dependencies/__init__.py b/tests/functional/typing/required_dependencies/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/tests/functional/test_utilities_typing.py b/tests/functional/typing/required_dependencies/test_utilities_typing.py
similarity index 100%
rename from tests/functional/test_utilities_typing.py
rename to tests/functional/typing/required_dependencies/test_utilities_typing.py
diff --git a/tests/functional/validator/_fastjsonschema/__init__.py b/tests/functional/validator/_fastjsonschema/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/tests/functional/validator/test_validator.py b/tests/functional/validator/_fastjsonschema/test_validator.py
similarity index 97%
rename from tests/functional/validator/test_validator.py
rename to tests/functional/validator/_fastjsonschema/test_validator.py
index 23b4943223a..f18787990ff 100644
--- a/tests/functional/validator/test_validator.py
+++ b/tests/functional/validator/_fastjsonschema/test_validator.py
@@ -83,6 +83,10 @@ def test_validate_invalid_custom_format(
)
+def test_validate_custom_handlers(schema_refs, schema_ref_handlers, parent_ref_event):
+ validate(event=parent_ref_event, schema=schema_refs["ParentSchema"], handlers=schema_ref_handlers)
+
+
def test_validate_invalid_envelope_expression(schema, wrapped_event):
with pytest.raises(exceptions.InvalidEnvelopeExpressionError):
validate(event=wrapped_event, schema=schema, envelope=True)
diff --git a/tests/functional/validator/conftest.py b/tests/functional/validator/conftest.py
index 750f7648d40..3b9033c82d4 100644
--- a/tests/functional/validator/conftest.py
+++ b/tests/functional/validator/conftest.py
@@ -85,6 +85,53 @@ def schema_response():
}
+@pytest.fixture
+def schema_refs():
+ return {
+ "ParentSchema": {
+ "$schema": "http://json-schema.org/draft-07/schema",
+ "$id": "testschema://ParentSchema",
+ "type": "object",
+ "title": "Sample schema",
+ "description": "Sample JSON Schema that references another schema",
+ "examples": [{"parent_object": {"child_string": "hello world"}}],
+ "required": ["parent_object"],
+ "properties": {
+ "parent_object": {
+ "$id": "#/properties/parent_object",
+ "$ref": "testschema://ChildSchema",
+ },
+ },
+ },
+ "ChildSchema": {
+ "$schema": "http://json-schema.org/draft-07/schema",
+ "$id": "testschema://ChildSchema",
+ "type": "object",
+ "title": "Sample schema",
+ "description": "Sample JSON Schema that is referenced by another schema",
+ "examples": [{"child_string": "hello world"}],
+ "required": ["child_string"],
+ "properties": {
+ "child_string": {
+ "$id": "#/properties/child_string",
+ "type": "string",
+ "title": "The child string",
+ "examples": ["hello world"],
+ },
+ },
+ },
+ }
+
+
+@pytest.fixture
+def schema_ref_handlers(schema_refs):
+ def handle_test_schema(uri):
+ schema_key = uri.split("://")[1]
+ return schema_refs[schema_key]
+
+ return {"testschema": handle_test_schema}
+
+
@pytest.fixture
def raw_event():
return {"message": "hello hello", "username": "blah blah"}
@@ -105,6 +152,11 @@ def wrapped_event_base64_json_string():
return {"data": "eyJtZXNzYWdlIjogImhlbGxvIGhlbGxvIiwgInVzZXJuYW1lIjogImJsYWggYmxhaCJ9="}
+@pytest.fixture
+def parent_ref_event():
+ return {"parent_object": {"child_string": "hello world"}}
+
+
@pytest.fixture
def raw_response():
return {"statusCode": 200, "body": "response"}
diff --git a/tests/unit/data_classes/_boto3/__init__.py b/tests/unit/data_classes/_boto3/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/tests/unit/data_classes/test_code_pipeline_job_event.py b/tests/unit/data_classes/_boto3/test_code_pipeline_job_event.py
similarity index 100%
rename from tests/unit/data_classes/test_code_pipeline_job_event.py
rename to tests/unit/data_classes/_boto3/test_code_pipeline_job_event.py
diff --git a/tests/unit/data_classes/required_dependencies/__init__.py b/tests/unit/data_classes/required_dependencies/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/tests/unit/data_classes/test_active_mq_event.py b/tests/unit/data_classes/required_dependencies/test_active_mq_event.py
similarity index 100%
rename from tests/unit/data_classes/test_active_mq_event.py
rename to tests/unit/data_classes/required_dependencies/test_active_mq_event.py
diff --git a/tests/unit/data_classes/test_alb_event.py b/tests/unit/data_classes/required_dependencies/test_alb_event.py
similarity index 100%
rename from tests/unit/data_classes/test_alb_event.py
rename to tests/unit/data_classes/required_dependencies/test_alb_event.py
diff --git a/tests/unit/data_classes/test_api_gateway_authorizer.py b/tests/unit/data_classes/required_dependencies/test_api_gateway_authorizer.py
similarity index 100%
rename from tests/unit/data_classes/test_api_gateway_authorizer.py
rename to tests/unit/data_classes/required_dependencies/test_api_gateway_authorizer.py
diff --git a/tests/unit/data_classes/test_api_gateway_authorizer_event.py b/tests/unit/data_classes/required_dependencies/test_api_gateway_authorizer_event.py
similarity index 100%
rename from tests/unit/data_classes/test_api_gateway_authorizer_event.py
rename to tests/unit/data_classes/required_dependencies/test_api_gateway_authorizer_event.py
diff --git a/tests/unit/data_classes/test_api_gateway_proxy_event.py b/tests/unit/data_classes/required_dependencies/test_api_gateway_proxy_event.py
similarity index 100%
rename from tests/unit/data_classes/test_api_gateway_proxy_event.py
rename to tests/unit/data_classes/required_dependencies/test_api_gateway_proxy_event.py
diff --git a/tests/unit/data_classes/test_appsync_authorizer_event.py b/tests/unit/data_classes/required_dependencies/test_appsync_authorizer_event.py
similarity index 100%
rename from tests/unit/data_classes/test_appsync_authorizer_event.py
rename to tests/unit/data_classes/required_dependencies/test_appsync_authorizer_event.py
diff --git a/tests/unit/data_classes/test_appsync_resolver_event.py b/tests/unit/data_classes/required_dependencies/test_appsync_resolver_event.py
similarity index 98%
rename from tests/unit/data_classes/test_appsync_resolver_event.py
rename to tests/unit/data_classes/required_dependencies/test_appsync_resolver_event.py
index a1a010c251a..ace6032e131 100644
--- a/tests/unit/data_classes/test_appsync_resolver_event.py
+++ b/tests/unit/data_classes/required_dependencies/test_appsync_resolver_event.py
@@ -80,7 +80,7 @@ def test_appsync_resolver_direct():
raw_event = load_event("appSyncDirectResolver.json")
parsed_event = AppSyncResolverEvent(raw_event)
- assert parsed_event.source is None
+ assert parsed_event.source == {}
assert parsed_event.arguments.get("id") == raw_event["arguments"]["id"]
assert parsed_event.stash == {}
assert parsed_event.prev_result is None
@@ -112,7 +112,7 @@ def test_appsync_resolver_event_info():
event = AppSyncResolverEvent(event)
- assert event.source is None
+ assert event.source == {}
assert event.identity is None
assert event.info is not None
assert isinstance(event.info, AppSyncResolverEventInfo)
diff --git a/tests/unit/data_classes/test_aws_config_rule_event.py b/tests/unit/data_classes/required_dependencies/test_aws_config_rule_event.py
similarity index 100%
rename from tests/unit/data_classes/test_aws_config_rule_event.py
rename to tests/unit/data_classes/required_dependencies/test_aws_config_rule_event.py
diff --git a/tests/unit/data_classes/test_bedrock_agent_event.py b/tests/unit/data_classes/required_dependencies/test_bedrock_agent_event.py
similarity index 100%
rename from tests/unit/data_classes/test_bedrock_agent_event.py
rename to tests/unit/data_classes/required_dependencies/test_bedrock_agent_event.py
diff --git a/tests/unit/data_classes/test_cloud_watch_alarm_event.py b/tests/unit/data_classes/required_dependencies/test_cloud_watch_alarm_event.py
similarity index 100%
rename from tests/unit/data_classes/test_cloud_watch_alarm_event.py
rename to tests/unit/data_classes/required_dependencies/test_cloud_watch_alarm_event.py
diff --git a/tests/unit/data_classes/test_cloud_watch_custom_widget_event.py b/tests/unit/data_classes/required_dependencies/test_cloud_watch_custom_widget_event.py
similarity index 100%
rename from tests/unit/data_classes/test_cloud_watch_custom_widget_event.py
rename to tests/unit/data_classes/required_dependencies/test_cloud_watch_custom_widget_event.py
diff --git a/tests/unit/data_classes/test_cloud_watch_logs_event.py b/tests/unit/data_classes/required_dependencies/test_cloud_watch_logs_event.py
similarity index 100%
rename from tests/unit/data_classes/test_cloud_watch_logs_event.py
rename to tests/unit/data_classes/required_dependencies/test_cloud_watch_logs_event.py
diff --git a/tests/unit/data_classes/test_cloudformation_custom_resource_event.py b/tests/unit/data_classes/required_dependencies/test_cloudformation_custom_resource_event.py
similarity index 100%
rename from tests/unit/data_classes/test_cloudformation_custom_resource_event.py
rename to tests/unit/data_classes/required_dependencies/test_cloudformation_custom_resource_event.py
diff --git a/tests/unit/data_classes/test_cognito_user_pool_event.py b/tests/unit/data_classes/required_dependencies/test_cognito_user_pool_event.py
similarity index 100%
rename from tests/unit/data_classes/test_cognito_user_pool_event.py
rename to tests/unit/data_classes/required_dependencies/test_cognito_user_pool_event.py
diff --git a/tests/unit/data_classes/test_connect_contact_flow_event.py b/tests/unit/data_classes/required_dependencies/test_connect_contact_flow_event.py
similarity index 100%
rename from tests/unit/data_classes/test_connect_contact_flow_event.py
rename to tests/unit/data_classes/required_dependencies/test_connect_contact_flow_event.py
diff --git a/tests/unit/data_classes/test_dynamo_db_stream_event.py b/tests/unit/data_classes/required_dependencies/test_dynamo_db_stream_event.py
similarity index 100%
rename from tests/unit/data_classes/test_dynamo_db_stream_event.py
rename to tests/unit/data_classes/required_dependencies/test_dynamo_db_stream_event.py
diff --git a/tests/unit/data_classes/test_event_bridge_event.py b/tests/unit/data_classes/required_dependencies/test_event_bridge_event.py
similarity index 100%
rename from tests/unit/data_classes/test_event_bridge_event.py
rename to tests/unit/data_classes/required_dependencies/test_event_bridge_event.py
diff --git a/tests/unit/data_classes/test_kafka_event.py b/tests/unit/data_classes/required_dependencies/test_kafka_event.py
similarity index 100%
rename from tests/unit/data_classes/test_kafka_event.py
rename to tests/unit/data_classes/required_dependencies/test_kafka_event.py
diff --git a/tests/unit/data_classes/test_kinesis_firehose_event.py b/tests/unit/data_classes/required_dependencies/test_kinesis_firehose_event.py
similarity index 100%
rename from tests/unit/data_classes/test_kinesis_firehose_event.py
rename to tests/unit/data_classes/required_dependencies/test_kinesis_firehose_event.py
diff --git a/tests/unit/data_classes/test_kinesis_firehose_response.py b/tests/unit/data_classes/required_dependencies/test_kinesis_firehose_response.py
similarity index 100%
rename from tests/unit/data_classes/test_kinesis_firehose_response.py
rename to tests/unit/data_classes/required_dependencies/test_kinesis_firehose_response.py
diff --git a/tests/unit/data_classes/test_kinesis_stream_event.py b/tests/unit/data_classes/required_dependencies/test_kinesis_stream_event.py
similarity index 100%
rename from tests/unit/data_classes/test_kinesis_stream_event.py
rename to tests/unit/data_classes/required_dependencies/test_kinesis_stream_event.py
diff --git a/tests/unit/data_classes/test_lambda_function_url.py b/tests/unit/data_classes/required_dependencies/test_lambda_function_url.py
similarity index 100%
rename from tests/unit/data_classes/test_lambda_function_url.py
rename to tests/unit/data_classes/required_dependencies/test_lambda_function_url.py
diff --git a/tests/unit/data_classes/test_rabbit_mq_event.py b/tests/unit/data_classes/required_dependencies/test_rabbit_mq_event.py
similarity index 100%
rename from tests/unit/data_classes/test_rabbit_mq_event.py
rename to tests/unit/data_classes/required_dependencies/test_rabbit_mq_event.py
diff --git a/tests/unit/data_classes/test_s3_batch_operation_event.py b/tests/unit/data_classes/required_dependencies/test_s3_batch_operation_event.py
similarity index 100%
rename from tests/unit/data_classes/test_s3_batch_operation_event.py
rename to tests/unit/data_classes/required_dependencies/test_s3_batch_operation_event.py
diff --git a/tests/unit/data_classes/test_s3_batch_operation_response.py b/tests/unit/data_classes/required_dependencies/test_s3_batch_operation_response.py
similarity index 100%
rename from tests/unit/data_classes/test_s3_batch_operation_response.py
rename to tests/unit/data_classes/required_dependencies/test_s3_batch_operation_response.py
diff --git a/tests/unit/data_classes/test_s3_event.py b/tests/unit/data_classes/required_dependencies/test_s3_event.py
similarity index 100%
rename from tests/unit/data_classes/test_s3_event.py
rename to tests/unit/data_classes/required_dependencies/test_s3_event.py
diff --git a/tests/unit/data_classes/test_s3_eventbridge_notification.py b/tests/unit/data_classes/required_dependencies/test_s3_eventbridge_notification.py
similarity index 100%
rename from tests/unit/data_classes/test_s3_eventbridge_notification.py
rename to tests/unit/data_classes/required_dependencies/test_s3_eventbridge_notification.py
diff --git a/tests/unit/data_classes/test_s3_object_event.py b/tests/unit/data_classes/required_dependencies/test_s3_object_event.py
similarity index 100%
rename from tests/unit/data_classes/test_s3_object_event.py
rename to tests/unit/data_classes/required_dependencies/test_s3_object_event.py
diff --git a/tests/unit/data_classes/test_secrets_manager_event.py b/tests/unit/data_classes/required_dependencies/test_secrets_manager_event.py
similarity index 100%
rename from tests/unit/data_classes/test_secrets_manager_event.py
rename to tests/unit/data_classes/required_dependencies/test_secrets_manager_event.py
diff --git a/tests/unit/data_classes/test_ses_event.py b/tests/unit/data_classes/required_dependencies/test_ses_event.py
similarity index 100%
rename from tests/unit/data_classes/test_ses_event.py
rename to tests/unit/data_classes/required_dependencies/test_ses_event.py
diff --git a/tests/unit/data_classes/test_sns_event.py b/tests/unit/data_classes/required_dependencies/test_sns_event.py
similarity index 100%
rename from tests/unit/data_classes/test_sns_event.py
rename to tests/unit/data_classes/required_dependencies/test_sns_event.py
diff --git a/tests/unit/data_classes/test_sqs_event.py b/tests/unit/data_classes/required_dependencies/test_sqs_event.py
similarity index 100%
rename from tests/unit/data_classes/test_sqs_event.py
rename to tests/unit/data_classes/required_dependencies/test_sqs_event.py
diff --git a/tests/unit/data_classes/test_vpc_lattice_event.py b/tests/unit/data_classes/required_dependencies/test_vpc_lattice_event.py
similarity index 100%
rename from tests/unit/data_classes/test_vpc_lattice_event.py
rename to tests/unit/data_classes/required_dependencies/test_vpc_lattice_event.py
diff --git a/tests/unit/data_classes/test_vpc_lattice_eventv2.py b/tests/unit/data_classes/required_dependencies/test_vpc_lattice_eventv2.py
similarity index 100%
rename from tests/unit/data_classes/test_vpc_lattice_eventv2.py
rename to tests/unit/data_classes/required_dependencies/test_vpc_lattice_eventv2.py
diff --git a/tests/unit/data_masking/__init__.py b/tests/unit/data_masking/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/tests/unit/data_masking/_aws_encryption_sdk/__init__.py b/tests/unit/data_masking/_aws_encryption_sdk/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/tests/unit/data_masking/test_kms_provider.py b/tests/unit/data_masking/_aws_encryption_sdk/test_kms_provider.py
similarity index 100%
rename from tests/unit/data_masking/test_kms_provider.py
rename to tests/unit/data_masking/_aws_encryption_sdk/test_kms_provider.py
diff --git a/tests/unit/data_masking/test_unit_data_masking.py b/tests/unit/data_masking/_aws_encryption_sdk/test_unit_data_masking.py
similarity index 100%
rename from tests/unit/data_masking/test_unit_data_masking.py
rename to tests/unit/data_masking/_aws_encryption_sdk/test_unit_data_masking.py
diff --git a/tests/unit/parser/_pydantic/__init__.py b/tests/unit/parser/_pydantic/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/tests/unit/parser/schemas.py b/tests/unit/parser/_pydantic/schemas.py
similarity index 100%
rename from tests/unit/parser/schemas.py
rename to tests/unit/parser/_pydantic/schemas.py
diff --git a/tests/unit/parser/test_alb.py b/tests/unit/parser/_pydantic/test_alb.py
similarity index 100%
rename from tests/unit/parser/test_alb.py
rename to tests/unit/parser/_pydantic/test_alb.py
diff --git a/tests/unit/parser/test_apigw.py b/tests/unit/parser/_pydantic/test_apigw.py
similarity index 99%
rename from tests/unit/parser/test_apigw.py
rename to tests/unit/parser/_pydantic/test_apigw.py
index b2ed294ff7a..7b4b2528373 100644
--- a/tests/unit/parser/test_apigw.py
+++ b/tests/unit/parser/_pydantic/test_apigw.py
@@ -4,7 +4,7 @@
from aws_lambda_powertools.utilities.parser import envelopes, parse
from aws_lambda_powertools.utilities.parser.models import APIGatewayProxyEventModel
from tests.functional.utils import load_event
-from tests.unit.parser.schemas import MyApiGatewayBusiness
+from tests.unit.parser._pydantic.schemas import MyApiGatewayBusiness
def test_apigw_event_with_envelope():
diff --git a/tests/unit/parser/test_apigwv2.py b/tests/unit/parser/_pydantic/test_apigwv2.py
similarity index 98%
rename from tests/unit/parser/test_apigwv2.py
rename to tests/unit/parser/_pydantic/test_apigwv2.py
index 5a0f627b3cd..47e79cbaa36 100644
--- a/tests/unit/parser/test_apigwv2.py
+++ b/tests/unit/parser/_pydantic/test_apigwv2.py
@@ -5,7 +5,7 @@
RequestContextV2Authorizer,
)
from tests.functional.utils import load_event
-from tests.unit.parser.schemas import MyApiGatewayBusiness
+from tests.unit.parser._pydantic.schemas import MyApiGatewayBusiness
def test_apigw_v2_event_with_envelope():
diff --git a/tests/unit/parser/test_bedrock_agent.py b/tests/unit/parser/_pydantic/test_bedrock_agent.py
similarity index 97%
rename from tests/unit/parser/test_bedrock_agent.py
rename to tests/unit/parser/_pydantic/test_bedrock_agent.py
index f3c208469e9..207318952cc 100644
--- a/tests/unit/parser/test_bedrock_agent.py
+++ b/tests/unit/parser/_pydantic/test_bedrock_agent.py
@@ -1,7 +1,7 @@
from aws_lambda_powertools.utilities.parser import envelopes, parse
from aws_lambda_powertools.utilities.parser.models import BedrockAgentEventModel
from tests.functional.utils import load_event
-from tests.unit.parser.schemas import MyBedrockAgentBusiness
+from tests.unit.parser._pydantic.schemas import MyBedrockAgentBusiness
def test_bedrock_agent_event_with_envelope():
diff --git a/tests/unit/parser/test_cloudformation_custom_resource.py b/tests/unit/parser/_pydantic/test_cloudformation_custom_resource.py
similarity index 100%
rename from tests/unit/parser/test_cloudformation_custom_resource.py
rename to tests/unit/parser/_pydantic/test_cloudformation_custom_resource.py
diff --git a/tests/unit/parser/test_cloudwatch.py b/tests/unit/parser/_pydantic/test_cloudwatch.py
similarity index 98%
rename from tests/unit/parser/test_cloudwatch.py
rename to tests/unit/parser/_pydantic/test_cloudwatch.py
index b62116dedbd..b7cf1801865 100644
--- a/tests/unit/parser/test_cloudwatch.py
+++ b/tests/unit/parser/_pydantic/test_cloudwatch.py
@@ -11,7 +11,7 @@
CloudWatchLogsModel,
)
from tests.functional.utils import load_event
-from tests.unit.parser.schemas import MyCloudWatchBusiness
+from tests.unit.parser._pydantic.schemas import MyCloudWatchBusiness
def decode_cloudwatch_raw_event(event: dict):
diff --git a/tests/unit/parser/test_dynamodb.py b/tests/unit/parser/_pydantic/test_dynamodb.py
similarity index 97%
rename from tests/unit/parser/test_dynamodb.py
rename to tests/unit/parser/_pydantic/test_dynamodb.py
index abbcd152d6b..d4014cd031f 100644
--- a/tests/unit/parser/test_dynamodb.py
+++ b/tests/unit/parser/_pydantic/test_dynamodb.py
@@ -2,7 +2,7 @@
from aws_lambda_powertools.utilities.parser import ValidationError, envelopes, parse
from tests.functional.utils import load_event
-from tests.unit.parser.schemas import MyAdvancedDynamoBusiness, MyDynamoBusiness
+from tests.unit.parser._pydantic.schemas import MyAdvancedDynamoBusiness, MyDynamoBusiness
def test_dynamo_db_stream_trigger_event():
diff --git a/tests/unit/parser/test_eventbridge.py b/tests/unit/parser/_pydantic/test_eventbridge.py
similarity index 97%
rename from tests/unit/parser/test_eventbridge.py
rename to tests/unit/parser/_pydantic/test_eventbridge.py
index 7f250ecdb83..056a3bb2591 100644
--- a/tests/unit/parser/test_eventbridge.py
+++ b/tests/unit/parser/_pydantic/test_eventbridge.py
@@ -2,7 +2,7 @@
from aws_lambda_powertools.utilities.parser import ValidationError, envelopes, parse
from tests.functional.utils import load_event
-from tests.unit.parser.schemas import (
+from tests.unit.parser._pydantic.schemas import (
MyAdvancedEventbridgeBusiness,
MyEventbridgeBusiness,
)
diff --git a/tests/unit/parser/test_kafka.py b/tests/unit/parser/_pydantic/test_kafka.py
similarity index 97%
rename from tests/unit/parser/test_kafka.py
rename to tests/unit/parser/_pydantic/test_kafka.py
index 1f229c1db6e..066820c2f11 100644
--- a/tests/unit/parser/test_kafka.py
+++ b/tests/unit/parser/_pydantic/test_kafka.py
@@ -5,7 +5,7 @@
KafkaSelfManagedEventModel,
)
from tests.functional.utils import load_event
-from tests.unit.parser.schemas import MyLambdaKafkaBusiness
+from tests.unit.parser._pydantic.schemas import MyLambdaKafkaBusiness
def test_kafka_msk_event_with_envelope():
diff --git a/tests/unit/parser/test_kinesis.py b/tests/unit/parser/_pydantic/test_kinesis.py
similarity index 98%
rename from tests/unit/parser/test_kinesis.py
rename to tests/unit/parser/_pydantic/test_kinesis.py
index 730759f1230..9da19ed3e0b 100644
--- a/tests/unit/parser/test_kinesis.py
+++ b/tests/unit/parser/_pydantic/test_kinesis.py
@@ -13,7 +13,7 @@
extract_cloudwatch_logs_from_record,
)
from tests.functional.utils import load_event
-from tests.unit.parser.schemas import MyKinesisBusiness
+from tests.unit.parser._pydantic.schemas import MyKinesisBusiness
def test_kinesis_trigger_bad_base64_event():
diff --git a/tests/unit/parser/test_kinesis_firehose.py b/tests/unit/parser/_pydantic/test_kinesis_firehose.py
similarity index 98%
rename from tests/unit/parser/test_kinesis_firehose.py
rename to tests/unit/parser/_pydantic/test_kinesis_firehose.py
index bd12d25e3d3..e12b0427110 100644
--- a/tests/unit/parser/test_kinesis_firehose.py
+++ b/tests/unit/parser/_pydantic/test_kinesis_firehose.py
@@ -9,7 +9,7 @@
KinesisFirehoseSqsRecord,
)
from tests.functional.utils import load_event
-from tests.unit.parser.schemas import MyKinesisFirehoseBusiness
+from tests.unit.parser._pydantic.schemas import MyKinesisFirehoseBusiness
def test_firehose_sqs_wrapped_message_event():
diff --git a/tests/unit/parser/test_lambda_function_url.py b/tests/unit/parser/_pydantic/test_lambda_function_url.py
similarity index 98%
rename from tests/unit/parser/test_lambda_function_url.py
rename to tests/unit/parser/_pydantic/test_lambda_function_url.py
index 3b1a7f259ec..8cf4c395e84 100644
--- a/tests/unit/parser/test_lambda_function_url.py
+++ b/tests/unit/parser/_pydantic/test_lambda_function_url.py
@@ -1,7 +1,7 @@
from aws_lambda_powertools.utilities.parser import envelopes, parse
from aws_lambda_powertools.utilities.parser.models import LambdaFunctionUrlModel
from tests.functional.utils import load_event
-from tests.unit.parser.schemas import MyALambdaFuncUrlBusiness
+from tests.unit.parser._pydantic.schemas import MyALambdaFuncUrlBusiness
def test_lambda_func_url_event_with_envelope():
diff --git a/tests/unit/parser/test_s3.py b/tests/unit/parser/_pydantic/test_s3.py
similarity index 100%
rename from tests/unit/parser/test_s3.py
rename to tests/unit/parser/_pydantic/test_s3.py
diff --git a/tests/unit/parser/test_s3_batch_operation.py b/tests/unit/parser/_pydantic/test_s3_batch_operation.py
similarity index 100%
rename from tests/unit/parser/test_s3_batch_operation.py
rename to tests/unit/parser/_pydantic/test_s3_batch_operation.py
diff --git a/tests/unit/parser/test_s3_notification.py b/tests/unit/parser/_pydantic/test_s3_notification.py
similarity index 100%
rename from tests/unit/parser/test_s3_notification.py
rename to tests/unit/parser/_pydantic/test_s3_notification.py
diff --git a/tests/unit/parser/test_s3_object_event.py b/tests/unit/parser/_pydantic/test_s3_object_event.py
similarity index 100%
rename from tests/unit/parser/test_s3_object_event.py
rename to tests/unit/parser/_pydantic/test_s3_object_event.py
diff --git a/tests/unit/parser/test_ses.py b/tests/unit/parser/_pydantic/test_ses.py
similarity index 100%
rename from tests/unit/parser/test_ses.py
rename to tests/unit/parser/_pydantic/test_ses.py
diff --git a/tests/unit/parser/test_sns.py b/tests/unit/parser/_pydantic/test_sns.py
similarity index 98%
rename from tests/unit/parser/test_sns.py
rename to tests/unit/parser/_pydantic/test_sns.py
index 9b925d5fa76..cfb0a5a820b 100644
--- a/tests/unit/parser/test_sns.py
+++ b/tests/unit/parser/_pydantic/test_sns.py
@@ -5,7 +5,7 @@
from aws_lambda_powertools.utilities.parser import ValidationError, envelopes, parse
from tests.functional.utils import load_event
from tests.functional.validator.conftest import sns_event # noqa: F401
-from tests.unit.parser.schemas import MyAdvancedSnsBusiness, MySnsBusiness
+from tests.unit.parser._pydantic.schemas import MyAdvancedSnsBusiness, MySnsBusiness
def test_handle_sns_trigger_event_json_body(sns_event): # noqa: F811
diff --git a/tests/unit/parser/test_sqs.py b/tests/unit/parser/_pydantic/test_sqs.py
similarity index 98%
rename from tests/unit/parser/test_sqs.py
rename to tests/unit/parser/_pydantic/test_sqs.py
index d28f1093d15..44fe44839ae 100644
--- a/tests/unit/parser/test_sqs.py
+++ b/tests/unit/parser/_pydantic/test_sqs.py
@@ -4,7 +4,7 @@
from aws_lambda_powertools.utilities.parser.models import SqsModel
from tests.functional.utils import load_event
from tests.functional.validator.conftest import sqs_event # noqa: F401
-from tests.unit.parser.schemas import MyAdvancedSqsBusiness, MySqsBusiness
+from tests.unit.parser._pydantic.schemas import MyAdvancedSqsBusiness, MySqsBusiness
def test_handle_sqs_trigger_event_json_body(sqs_event): # noqa: F811
diff --git a/tests/unit/parser/test_vpc_lattice.py b/tests/unit/parser/_pydantic/test_vpc_lattice.py
similarity index 95%
rename from tests/unit/parser/test_vpc_lattice.py
rename to tests/unit/parser/_pydantic/test_vpc_lattice.py
index e5dfedfb445..0ffd919e4db 100644
--- a/tests/unit/parser/test_vpc_lattice.py
+++ b/tests/unit/parser/_pydantic/test_vpc_lattice.py
@@ -3,7 +3,7 @@
from aws_lambda_powertools.utilities.parser import ValidationError, envelopes, parse
from aws_lambda_powertools.utilities.parser.models import VpcLatticeModel
from tests.functional.utils import load_event
-from tests.unit.parser.schemas import MyVpcLatticeBusiness
+from tests.unit.parser._pydantic.schemas import MyVpcLatticeBusiness
def test_vpc_lattice_event_with_envelope():
diff --git a/tests/unit/parser/test_vpc_latticev2.py b/tests/unit/parser/_pydantic/test_vpc_latticev2.py
similarity index 97%
rename from tests/unit/parser/test_vpc_latticev2.py
rename to tests/unit/parser/_pydantic/test_vpc_latticev2.py
index 78d93fde041..d21dd84abe6 100644
--- a/tests/unit/parser/test_vpc_latticev2.py
+++ b/tests/unit/parser/_pydantic/test_vpc_latticev2.py
@@ -3,7 +3,7 @@
from aws_lambda_powertools.utilities.parser import ValidationError, envelopes, parse
from aws_lambda_powertools.utilities.parser.models import VpcLatticeV2Model
from tests.functional.utils import load_event
-from tests.unit.parser.schemas import MyVpcLatticeBusiness
+from tests.unit.parser._pydantic.schemas import MyVpcLatticeBusiness
def test_vpc_lattice_v2_event_with_envelope():
diff --git a/tests/unit/test_tracing.py b/tests/unit/test_tracing.py
index 0d12afa629b..2c52d8e9dc0 100644
--- a/tests/unit/test_tracing.py
+++ b/tests/unit/test_tracing.py
@@ -9,7 +9,7 @@
# Maintenance: This should move to Functional tests and use Fake over mocks.
-MODULE_PREFIX = "unit.test_tracing"
+MODULE_PREFIX = "tests.unit.test_tracing"
@pytest.fixture