diff --git a/aws_lambda_powertools/utilities/parameters/appconfig.py b/aws_lambda_powertools/utilities/parameters/appconfig.py index 3455617e952..380e355d673 100644 --- a/aws_lambda_powertools/utilities/parameters/appconfig.py +++ b/aws_lambda_powertools/utilities/parameters/appconfig.py @@ -4,12 +4,15 @@ import os -from typing import Any, Dict, Optional, Union +from typing import TYPE_CHECKING, Any, Dict, Optional, Union from uuid import uuid4 import boto3 from botocore.config import Config +if TYPE_CHECKING: + from mypy_boto3_appconfig import AppConfigClient + from ...shared import constants from ...shared.functions import resolve_env_var_choice from .base import DEFAULT_MAX_AGE_SECS, DEFAULT_PROVIDERS, BaseProvider @@ -30,7 +33,9 @@ class AppConfigProvider(BaseProvider): config: botocore.config.Config, optional Botocore configuration to pass during client initialization boto3_session : boto3.session.Session, optional - Boto3 session to use for AWS API communication + Boto3 session to create a boto3_client from + boto3_client: AppConfigClient, optional + Boto3 AppConfig Client to use, boto3_session will be ignored if both are provided Example ------- @@ -68,22 +73,24 @@ def __init__( application: Optional[str] = None, config: Optional[Config] = None, boto3_session: Optional[boto3.session.Session] = None, + boto3_client: Optional["AppConfigClient"] = None, ): """ Initialize the App Config client """ - config = config or Config() - session = boto3_session or boto3.session.Session() - self.client = session.client("appconfig", config=config) + super().__init__() + + self.client: "AppConfigClient" = self._build_boto3_client( + service_name="appconfig", client=boto3_client, session=boto3_session, config=config + ) + self.application = resolve_env_var_choice( choice=application, env=os.getenv(constants.SERVICE_NAME_ENV, "service_undefined") ) self.environment = environment self.current_version = "" - super().__init__() - def _get(self, name: str, **sdk_options) -> str: """ Retrieve a parameter value from AWS App config. diff --git a/aws_lambda_powertools/utilities/parameters/base.py b/aws_lambda_powertools/utilities/parameters/base.py index 9c6e74ffb00..ce03b757618 100644 --- a/aws_lambda_powertools/utilities/parameters/base.py +++ b/aws_lambda_powertools/utilities/parameters/base.py @@ -7,10 +7,20 @@ from abc import ABC, abstractmethod from collections import namedtuple from datetime import datetime, timedelta -from typing import Any, Dict, Optional, Tuple, Union +from typing import TYPE_CHECKING, Any, Dict, Optional, Tuple, Type, Union + +import boto3 +from botocore.config import Config from .exceptions import GetParameterError, TransformParameterError +if TYPE_CHECKING: + from mypy_boto3_appconfig import AppConfigClient + from mypy_boto3_dynamodb import DynamoDBServiceResource + from mypy_boto3_secretsmanager import SecretsManagerClient + from mypy_boto3_ssm import SSMClient + + DEFAULT_MAX_AGE_SECS = 5 ExpirableValue = namedtuple("ExpirableValue", ["value", "ttl"]) # These providers will be dynamically initialized on first use of the helper functions @@ -18,6 +28,7 @@ TRANSFORM_METHOD_JSON = "json" TRANSFORM_METHOD_BINARY = "binary" SUPPORTED_TRANSFORM_METHODS = [TRANSFORM_METHOD_JSON, TRANSFORM_METHOD_BINARY] +ParameterClients = Union["AppConfigClient", "SecretsManagerClient", "SSMClient"] class BaseProvider(ABC): @@ -180,6 +191,72 @@ def _get_multiple(self, path: str, **sdk_options) -> Dict[str, str]: def clear_cache(self): self.store.clear() + @staticmethod + def _build_boto3_client( + service_name: str, + client: Optional[ParameterClients] = None, + session: Optional[Type[boto3.Session]] = None, + config: Optional[Type[Config]] = None, + ) -> Type[ParameterClients]: + """Builds a low level boto3 client with session and config provided + + Parameters + ---------- + service_name : str + AWS service name to instantiate a boto3 client, e.g. ssm + client : Optional[ParameterClients], optional + boto3 client instance, by default None + session : Optional[Type[boto3.Session]], optional + boto3 session instance, by default None + config : Optional[Type[Config]], optional + botocore config instance to configure client with, by default None + + Returns + ------- + Type[ParameterClients] + Instance of a boto3 client for Parameters feature (e.g., ssm, appconfig, secretsmanager, etc.) + """ + if client is not None: + return client + + session = session or boto3.Session() + config = config or Config() + return session.client(service_name=service_name, config=config) + + # maintenance: change DynamoDBServiceResource type to ParameterResourceClients when we expand + @staticmethod + def _build_boto3_resource_client( + service_name: str, + client: Optional["DynamoDBServiceResource"] = None, + session: Optional[Type[boto3.Session]] = None, + config: Optional[Type[Config]] = None, + endpoint_url: Optional[str] = None, + ) -> "DynamoDBServiceResource": + """Builds a high level boto3 resource client with session, config and endpoint_url provided + + Parameters + ---------- + service_name : str + AWS service name to instantiate a boto3 client, e.g. ssm + client : Optional[DynamoDBServiceResource], optional + boto3 client instance, by default None + session : Optional[Type[boto3.Session]], optional + boto3 session instance, by default None + config : Optional[Type[Config]], optional + botocore config instance to configure client, by default None + + Returns + ------- + Type[DynamoDBServiceResource] + Instance of a boto3 resource client for Parameters feature (e.g., dynamodb, etc.) + """ + if client is not None: + return client + + session = session or boto3.Session() + config = config or Config() + return session.resource(service_name=service_name, config=config, endpoint_url=endpoint_url) + def get_transform_method(key: str, transform: Optional[str] = None) -> Optional[str]: """ diff --git a/aws_lambda_powertools/utilities/parameters/dynamodb.py b/aws_lambda_powertools/utilities/parameters/dynamodb.py index 9220edf3b05..612ddf827d3 100644 --- a/aws_lambda_powertools/utilities/parameters/dynamodb.py +++ b/aws_lambda_powertools/utilities/parameters/dynamodb.py @@ -3,7 +3,7 @@ """ -from typing import Dict, Optional +from typing import TYPE_CHECKING, Dict, Optional import boto3 from boto3.dynamodb.conditions import Key @@ -11,6 +11,10 @@ from .base import BaseProvider +if TYPE_CHECKING: + from mypy_boto3_dynamodb import DynamoDBServiceResource + from mypy_boto3_dynamodb.service_resource import Table + class DynamoDBProvider(BaseProvider): """ @@ -31,7 +35,9 @@ class DynamoDBProvider(BaseProvider): config: botocore.config.Config, optional Botocore configuration to pass during client initialization boto3_session : boto3.session.Session, optional - Boto3 session to use for AWS API communication + Boto3 session to create a boto3_client from + boto3_client: DynamoDBServiceResource, optional + Boto3 DynamoDB Resource Client to use; boto3_session will be ignored if both are provided Example ------- @@ -152,15 +158,18 @@ def __init__( endpoint_url: Optional[str] = None, config: Optional[Config] = None, boto3_session: Optional[boto3.session.Session] = None, + boto3_client: Optional["DynamoDBServiceResource"] = None, ): """ Initialize the DynamoDB client """ - - config = config or Config() - session = boto3_session or boto3.session.Session() - - self.table = session.resource("dynamodb", endpoint_url=endpoint_url, config=config).Table(table_name) + self.table: "Table" = self._build_boto3_resource_client( + service_name="dynamodb", + client=boto3_client, + session=boto3_session, + config=config, + endpoint_url=endpoint_url, + ).Table(table_name) self.key_attr = key_attr self.sort_attr = sort_attr @@ -183,7 +192,9 @@ def _get(self, name: str, **sdk_options) -> str: # Explicit arguments will take precedence over keyword arguments sdk_options["Key"] = {self.key_attr: name} - return self.table.get_item(**sdk_options)["Item"][self.value_attr] + # maintenance: look for better ways to correctly type DynamoDB multiple return types + # without a breaking change within ABC return type + return self.table.get_item(**sdk_options)["Item"][self.value_attr] # type: ignore[return-value] def _get_multiple(self, path: str, **sdk_options) -> Dict[str, str]: """ @@ -209,4 +220,6 @@ def _get_multiple(self, path: str, **sdk_options) -> Dict[str, str]: response = self.table.query(**sdk_options) items.extend(response.get("Items", [])) - return {item[self.sort_attr]: item[self.value_attr] for item in items} + # maintenance: look for better ways to correctly type DynamoDB multiple return types + # without a breaking change within ABC return type + return {item[self.sort_attr]: item[self.value_attr] for item in items} # type: ignore[misc] diff --git a/aws_lambda_powertools/utilities/parameters/secrets.py b/aws_lambda_powertools/utilities/parameters/secrets.py index b64e70ae184..affdaf2e4dd 100644 --- a/aws_lambda_powertools/utilities/parameters/secrets.py +++ b/aws_lambda_powertools/utilities/parameters/secrets.py @@ -3,11 +3,14 @@ """ -from typing import Any, Dict, Optional, Union +from typing import TYPE_CHECKING, Any, Dict, Optional, Union import boto3 from botocore.config import Config +if TYPE_CHECKING: + from mypy_boto3_secretsmanager import SecretsManagerClient + from .base import DEFAULT_MAX_AGE_SECS, DEFAULT_PROVIDERS, BaseProvider @@ -20,7 +23,9 @@ class SecretsProvider(BaseProvider): config: botocore.config.Config, optional Botocore configuration to pass during client initialization boto3_session : boto3.session.Session, optional - Boto3 session to use for AWS API communication + Boto3 session to create a boto3_client from + boto3_client: SecretsManagerClient, optional + Boto3 SecretsManager Client to use, boto3_session will be ignored if both are provided Example ------- @@ -60,17 +65,22 @@ class SecretsProvider(BaseProvider): client: Any = None - def __init__(self, config: Optional[Config] = None, boto3_session: Optional[boto3.session.Session] = None): + def __init__( + self, + config: Optional[Config] = None, + boto3_session: Optional[boto3.session.Session] = None, + boto3_client: Optional["SecretsManagerClient"] = None, + ): """ Initialize the Secrets Manager client """ - config = config or Config() - session = boto3_session or boto3.session.Session() - self.client = session.client("secretsmanager", config=config) - super().__init__() + self.client: "SecretsManagerClient" = self._build_boto3_client( + service_name="secretsmanager", client=boto3_client, session=boto3_session, config=config + ) + def _get(self, name: str, **sdk_options) -> str: """ Retrieve a parameter value from AWS Systems Manager Parameter Store diff --git a/aws_lambda_powertools/utilities/parameters/ssm.py b/aws_lambda_powertools/utilities/parameters/ssm.py index fd55e40a95f..3b3e782fd45 100644 --- a/aws_lambda_powertools/utilities/parameters/ssm.py +++ b/aws_lambda_powertools/utilities/parameters/ssm.py @@ -3,13 +3,16 @@ """ -from typing import Any, Dict, Optional, Union +from typing import TYPE_CHECKING, Any, Dict, Optional, Union import boto3 from botocore.config import Config from .base import DEFAULT_MAX_AGE_SECS, DEFAULT_PROVIDERS, BaseProvider +if TYPE_CHECKING: + from mypy_boto3_ssm import SSMClient + class SSMProvider(BaseProvider): """ @@ -20,7 +23,9 @@ class SSMProvider(BaseProvider): config: botocore.config.Config, optional Botocore configuration to pass during client initialization boto3_session : boto3.session.Session, optional - Boto3 session to use for AWS API communication + Boto3 session to create a boto3_client from + boto3_client: SSMClient, optional + Boto3 SSM Client to use, boto3_session will be ignored if both are provided Example ------- @@ -76,17 +81,22 @@ class SSMProvider(BaseProvider): client: Any = None - def __init__(self, config: Optional[Config] = None, boto3_session: Optional[boto3.session.Session] = None): + def __init__( + self, + config: Optional[Config] = None, + boto3_session: Optional[boto3.session.Session] = None, + boto3_client: Optional["SSMClient"] = None, + ): """ Initialize the SSM Parameter Store client """ - config = config or Config() - session = boto3_session or boto3.session.Session() - self.client = session.client("ssm", config=config) - super().__init__() + self.client: "SSMClient" = self._build_boto3_client( + service_name="ssm", client=boto3_client, session=boto3_session, config=config + ) + # We break Liskov substitution principle due to differences in signatures of this method and superclass get method # We ignore mypy error, as changes to the signature here or in a superclass is a breaking change to users def get( # type: ignore[override] diff --git a/docs/utilities/parameters.md b/docs/utilities/parameters.md index 6b63168f2d7..36990fdd2cb 100644 --- a/docs/utilities/parameters.md +++ b/docs/utilities/parameters.md @@ -24,14 +24,14 @@ This utility requires additional permissions to work as expected. ???+ note Different parameter providers require different permissions. -Provider | Function/Method | IAM Permission -------------------------------------------------- | ------------------------------------------------- | --------------------------------------------------------------------------------- -SSM Parameter Store | `get_parameter`, `SSMProvider.get` | `ssm:GetParameter` -SSM Parameter Store | `get_parameters`, `SSMProvider.get_multiple` | `ssm:GetParametersByPath` -Secrets Manager | `get_secret`, `SecretsManager.get` | `secretsmanager:GetSecretValue` -DynamoDB | `DynamoDBProvider.get` | `dynamodb:GetItem` -DynamoDB | `DynamoDBProvider.get_multiple` | `dynamodb:Query` -App Config | `AppConfigProvider.get_app_config`, `get_app_config` | `appconfig:GetConfiguration` +| Provider | Function/Method | IAM Permission | +| ------------------- | ---------------------------------------------------- | ------------------------------- | +| SSM Parameter Store | `get_parameter`, `SSMProvider.get` | `ssm:GetParameter` | +| SSM Parameter Store | `get_parameters`, `SSMProvider.get_multiple` | `ssm:GetParametersByPath` | +| Secrets Manager | `get_secret`, `SecretsManager.get` | `secretsmanager:GetSecretValue` | +| DynamoDB | `DynamoDBProvider.get` | `dynamodb:GetItem` | +| DynamoDB | `DynamoDBProvider.get_multiple` | `dynamodb:Query` | +| App Config | `AppConfigProvider.get_app_config`, `get_app_config` | `appconfig:GetConfiguration` | ### Fetching parameters @@ -147,10 +147,10 @@ def handler(event, context): The AWS Systems Manager Parameter Store provider supports two additional arguments for the `get()` and `get_multiple()` methods: -| Parameter | Default | Description | -|---------------|---------|-------------| -| **decrypt** | `False` | Will automatically decrypt the parameter. -| **recursive** | `True` | For `get_multiple()` only, will fetch all parameter values recursively based on a path prefix. +| Parameter | Default | Description | +| ------------- | ------- | ---------------------------------------------------------------------------------------------- | +| **decrypt** | `False` | Will automatically decrypt the parameter. | +| **recursive** | `True` | For `get_multiple()` only, will fetch all parameter values recursively based on a path prefix. | ```python hl_lines="6 8" title="Example with get() and get_multiple()" from aws_lambda_powertools.utilities import parameters @@ -189,9 +189,9 @@ For single parameters, you must use `id` as the [partition key](https://docs.aws DynamoDB table with `id` partition key and `value` as attribute - | id | value | - |--------------|----------| - | my-parameter | my-value | + | id | value | + | ------------ | -------- | + | my-parameter | my-value | With this table, `dynamodb_provider.get("my-param")` will return `my-value`. @@ -223,11 +223,11 @@ You can retrieve multiple parameters sharing the same `id` by having a sort key DynamoDB table with `id` primary key, `sk` as sort key` and `value` as attribute - | id | sk | value | - |-------------|---------|------------| - | my-hash-key | param-a | my-value-a | - | my-hash-key | param-b | my-value-b | - | my-hash-key | param-c | my-value-c | + | id | sk | value | + | ----------- | ------- | ---------- | + | my-hash-key | param-a | my-value-a | + | my-hash-key | param-b | my-value-b | + | my-hash-key | param-c | my-value-c | With this table, `dynamodb_provider.get_multiple("my-hash-key")` will return a dictionary response in the shape of `sk:value`. @@ -261,12 +261,12 @@ With this table, `dynamodb_provider.get_multiple("my-hash-key")` will return a d DynamoDB provider can be customized at initialization to match your table structure: -| Parameter | Mandatory | Default | Description | -|----------------|-----------|---------|-------------| -| **table_name** | **Yes** | *(N/A)* | Name of the DynamoDB table containing the parameter values. -| **key_attr** | No | `id` | Hash key for the DynamoDB table. -| **sort_attr** | No | `sk` | Range key for the DynamoDB table. You don't need to set this if you don't use the `get_multiple()` method. -| **value_attr** | No | `value` | Name of the attribute containing the parameter value. +| Parameter | Mandatory | Default | Description | +| -------------- | --------- | ------- | ---------------------------------------------------------------------------------------------------------- | +| **table_name** | **Yes** | *(N/A)* | Name of the DynamoDB table containing the parameter values. | +| **key_attr** | No | `id` | Hash key for the DynamoDB table. | +| **sort_attr** | No | `sk` | Range key for the DynamoDB table. You don't need to set this if you don't use the `get_multiple()` method. | +| **value_attr** | No | `value` | Name of the attribute containing the parameter value. | ```python hl_lines="3-8" title="Customizing DynamoDBProvider to suit your table design" from aws_lambda_powertools.utilities import parameters @@ -467,26 +467,66 @@ def handler(event, context): Here is the mapping between this utility's functions and methods and the underlying SDK: -| Provider | Function/Method | Client name | Function name | -|---------------------|---------------------------------|------------------|----------------| -| SSM Parameter Store | `get_parameter` | `ssm` | [get_parameter](https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/ssm.html#SSM.Client.get_parameter) | -| SSM Parameter Store | `get_parameters` | `ssm` | [get_parameters_by_path](https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/ssm.html#SSM.Client.get_parameters_by_path) | -| SSM Parameter Store | `SSMProvider.get` | `ssm` | [get_parameter](https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/ssm.html#SSM.Client.get_parameter) | -| SSM Parameter Store | `SSMProvider.get_multiple` | `ssm` | [get_parameters_by_path](https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/ssm.html#SSM.Client.get_parameters_by_path) | +| Provider | Function/Method | Client name | Function name | +| ------------------- | ------------------------------- | ---------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | +| SSM Parameter Store | `get_parameter` | `ssm` | [get_parameter](https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/ssm.html#SSM.Client.get_parameter) | +| SSM Parameter Store | `get_parameters` | `ssm` | [get_parameters_by_path](https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/ssm.html#SSM.Client.get_parameters_by_path) | +| SSM Parameter Store | `SSMProvider.get` | `ssm` | [get_parameter](https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/ssm.html#SSM.Client.get_parameter) | +| SSM Parameter Store | `SSMProvider.get_multiple` | `ssm` | [get_parameters_by_path](https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/ssm.html#SSM.Client.get_parameters_by_path) | | Secrets Manager | `get_secret` | `secretsmanager` | [get_secret_value](https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/secretsmanager.html#SecretsManager.Client.get_secret_value) | | Secrets Manager | `SecretsManager.get` | `secretsmanager` | [get_secret_value](https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/secretsmanager.html#SecretsManager.Client.get_secret_value) | -| DynamoDB | `DynamoDBProvider.get` | `dynamodb` | ([Table resource](https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/dynamodb.html#table)) | [get_item](https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/dynamodb.html#DynamoDB.Table.get_item) -| DynamoDB | `DynamoDBProvider.get_multiple` | `dynamodb` | ([Table resource](https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/dynamodb.html#table)) | [query](https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/dynamodb.html#DynamoDB.Table.query) -| App Config | `get_app_config` | `appconfig` | [get_configuration](https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/appconfig.html#AppConfig.Client.get_configuration) | +| DynamoDB | `DynamoDBProvider.get` | `dynamodb` | ([Table resource](https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/dynamodb.html#table)) | [get_item](https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/dynamodb.html#DynamoDB.Table.get_item) | +| DynamoDB | `DynamoDBProvider.get_multiple` | `dynamodb` | ([Table resource](https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/dynamodb.html#table)) | [query](https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/dynamodb.html#DynamoDB.Table.query) | +| App Config | `get_app_config` | `appconfig` | [get_configuration](https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/appconfig.html#AppConfig.Client.get_configuration) | +### Bring your own boto client + +You can use `boto3_client` parameter via any of the available [Provider Classes](#built-in-provider-class). Some providers expect a low level boto3 client while others expect a high level boto3 client, here is the mapping for each of them: + +| Provider | Type | Boto client construction | +| --------------------------------------- | ---------- | ---------------------------- | +| [SSMProvider](#ssmprovider) | low level | `boto3.client("ssm")` | +| [SecretsProvider](#secretsprovider) | low level | `boto3.client("secrets")` | +| [AppConfigProvider](#appconfigprovider) | low level | `boto3.client("appconfig")` | +| [DynamoDBProvider](#dynamodbprovider) | high level | `boto3.resource("dynamodb")` | + + +Bringing them together in a single code snippet would look like this: + +```python title="Example: passing a custom boto3 client for each provider" +import boto3 +from botocore.config import Config + +from aws_lambda_powertools.utilities import parameters + +config = Config(region_name="us-west-1") + +# construct boto clients with any custom configuration +ssm = boto3.client("ssm", config=config) +secrets = boto3.client("secrets", config=config) +appconfig = boto3.client("appconfig", config=config) +dynamodb = boto3.resource("dynamodb", config=config) + +ssm_provider = parameters.SSMProvider(boto3_client=ssm) +secrets_provider = parameters.SecretsProvider(boto3_client=secrets) +appconf_provider = parameters.AppConfigProvider(boto3_client=appconfig, environment="my_env", application="my_app") +dynamodb_provider = parameters.DynamoDBProvider(boto3_client=dynamodb, table_name="my-table") + +``` + +???+ question "When is this useful?" + Injecting a custom boto3 client can make unit/snapshot testing easier, including SDK customizations. + ### Customizing boto configuration -The **`config`** and **`boto3_session`** parameters enable you to pass in a custom [botocore config object](https://botocore.amazonaws.com/v1/documentation/api/latest/reference/config.html) or a custom [boto3 session](https://boto3.amazonaws.com/v1/documentation/api/latest/reference/core/session.html) when constructing any of the built-in provider classes. +The **`config`** , **`boto3_session`**, and **`boto3_client`** parameters enable you to pass in a custom [botocore config object](https://botocore.amazonaws.com/v1/documentation/api/latest/reference/config.html) , [boto3 session](https://boto3.amazonaws.com/v1/documentation/api/latest/reference/core/session.html), or a [boto3 client](https://boto3.amazonaws.com/v1/documentation/api/latest/reference/core/boto3.html) when constructing any of the built-in provider classes. ???+ tip You can use a custom session for retrieving parameters cross-account/region and for snapshot testing. + When using VPC private endpoints, you can pass a custom client altogether. It's also useful for testing when injecting fake instances. + === "Custom session" ```python hl_lines="2 4 5" @@ -516,6 +556,22 @@ The **`config`** and **`boto3_session`** parameters enable you to pass in a cust ... ``` +=== "Custom client" + + ```python hl_lines="2 4 5" + from aws_lambda_powertools.utilities import parameters + import boto3 + + boto3_client= boto3.client("ssm") + ssm_provider = parameters.SSMProvider(boto3_client=boto3_client) + + def handler(event, context): + # Retrieve a single parameter + value = ssm_provider.get("/my/parameter") + ... + ``` + + ## Testing your code ### Mocking parameter values diff --git a/poetry.lock b/poetry.lock index 32b9181a2d7..ef3694f3ec0 100644 --- a/poetry.lock +++ b/poetry.lock @@ -614,6 +614,50 @@ dmypy = ["psutil (>=4.0)"] python2 = ["typed-ast (>=1.4.0,<2)"] reports = ["lxml"] +[[package]] +name = "mypy-boto3-appconfig" +version = "1.21.34" +description = "Type annotations for boto3.AppConfig 1.21.34 service generated with mypy-boto3-builder 7.5.8" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +typing-extensions = ">=4.1.0" + +[[package]] +name = "mypy-boto3-dynamodb" +version = "1.23.0.post1" +description = "Type annotations for boto3.DynamoDB 1.23.0 service generated with mypy-boto3-builder 7.5.14" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +typing-extensions = ">=4.1.0" + +[[package]] +name = "mypy-boto3-secretsmanager" +version = "1.21.34" +description = "Type annotations for boto3.SecretsManager 1.21.34 service generated with mypy-boto3-builder 7.5.8" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +typing-extensions = ">=4.1.0" + +[[package]] +name = "mypy-boto3-ssm" +version = "1.21.34" +description = "Type annotations for boto3.SSM 1.21.34 service generated with mypy-boto3-builder 7.5.8" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +typing-extensions = ">=4.1.0" + [[package]] name = "mypy-extensions" version = "0.4.3" @@ -988,7 +1032,7 @@ python-versions = "*" [[package]] name = "typing-extensions" -version = "4.0.1" +version = "4.1.1" description = "Backported and Experimental Type Hints for Python 3.6+" category = "main" optional = false @@ -1057,7 +1101,7 @@ pydantic = ["pydantic", "email-validator"] [metadata] lock-version = "1.1" python-versions = "^3.6.2" -content-hash = "b730dd1c59fa3030c0280b255f1cbefca5afa4b86b3178fcf014461734e05042" +content-hash = "03b50d43366216805a13a1e258c0dcc5b82e543ada45264ea60ccb8c73cbfe62" [metadata.files] atomicwrites = [ @@ -1263,28 +1307,12 @@ markdown = [ {file = "Markdown-3.3.5.tar.gz", hash = "sha256:26e9546bfbcde5fcd072bd8f612c9c1b6e2677cb8aadbdf65206674f46dde069"}, ] markupsafe = [ - {file = "MarkupSafe-2.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d8446c54dc28c01e5a2dbac5a25f071f6653e6e40f3a8818e8b45d790fe6ef53"}, - {file = "MarkupSafe-2.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:36bc903cbb393720fad60fc28c10de6acf10dc6cc883f3e24ee4012371399a38"}, - {file = "MarkupSafe-2.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2d7d807855b419fc2ed3e631034685db6079889a1f01d5d9dac950f764da3dad"}, - {file = "MarkupSafe-2.0.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:add36cb2dbb8b736611303cd3bfcee00afd96471b09cda130da3581cbdc56a6d"}, - {file = "MarkupSafe-2.0.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:168cd0a3642de83558a5153c8bd34f175a9a6e7f6dc6384b9655d2697312a646"}, - {file = "MarkupSafe-2.0.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:4dc8f9fb58f7364b63fd9f85013b780ef83c11857ae79f2feda41e270468dd9b"}, - {file = "MarkupSafe-2.0.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:20dca64a3ef2d6e4d5d615a3fd418ad3bde77a47ec8a23d984a12b5b4c74491a"}, - {file = "MarkupSafe-2.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:cdfba22ea2f0029c9261a4bd07e830a8da012291fbe44dc794e488b6c9bb353a"}, - {file = "MarkupSafe-2.0.1-cp310-cp310-win32.whl", hash = "sha256:99df47edb6bda1249d3e80fdabb1dab8c08ef3975f69aed437cb69d0a5de1e28"}, - {file = "MarkupSafe-2.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:e0f138900af21926a02425cf736db95be9f4af72ba1bb21453432a07f6082134"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:f9081981fe268bd86831e5c75f7de206ef275defcb82bc70740ae6dc507aee51"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:0955295dd5eec6cb6cc2fe1698f4c6d84af2e92de33fbcac4111913cd100a6ff"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:0446679737af14f45767963a1a9ef7620189912317d095f2d9ffa183a4d25d2b"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:f826e31d18b516f653fe296d967d700fddad5901ae07c622bb3705955e1faa94"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:fa130dd50c57d53368c9d59395cb5526eda596d3ffe36666cd81a44d56e48872"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:905fec760bd2fa1388bb5b489ee8ee5f7291d692638ea5f67982d968366bef9f"}, - {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bf5d821ffabf0ef3533c39c518f3357b171a1651c1ff6827325e4489b0e46c3c"}, - {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:0d4b31cc67ab36e3392bbf3862cfbadac3db12bdd8b02a2731f509ed5b829724"}, - {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:baa1a4e8f868845af802979fcdbf0bb11f94f1cb7ced4c4b8a351bb60d108145"}, - {file = "MarkupSafe-2.0.1-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:deb993cacb280823246a026e3b2d81c493c53de6acfd5e6bfe31ab3402bb37dd"}, - {file = "MarkupSafe-2.0.1-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:63f3268ba69ace99cab4e3e3b5840b03340efed0948ab8f78d2fd87ee5442a4f"}, - {file = "MarkupSafe-2.0.1-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:8d206346619592c6200148b01a2142798c989edcb9c896f9ac9722a99d4e77e6"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-win32.whl", hash = "sha256:6c4ca60fa24e85fe25b912b01e62cb969d69a23a5d5867682dd3e80b5b02581d"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b2f4bf27480f5e5e8ce285a8c8fd176c0b03e93dcc6646477d4630e83440c6a9"}, {file = "MarkupSafe-2.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0717a7390a68be14b8c793ba258e075c6f4ca819f15edfc2a3a027c823718567"}, @@ -1293,27 +1321,14 @@ markupsafe = [ {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:d7f9850398e85aba693bb640262d3611788b1f29a79f0c93c565694658f4071f"}, {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:6a7fae0dd14cf60ad5ff42baa2e95727c3d81ded453457771d02b7d2b3f9c0c2"}, {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:b7f2d075102dc8c794cbde1947378051c4e5180d52d276987b8d28a3bd58c17d"}, - {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e9936f0b261d4df76ad22f8fee3ae83b60d7c3e871292cd42f40b81b70afae85"}, - {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:2a7d351cbd8cfeb19ca00de495e224dea7e7d919659c2841bbb7f420ad03e2d6"}, - {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:60bf42e36abfaf9aff1f50f52644b336d4f0a3fd6d8a60ca0d054ac9f713a864"}, - {file = "MarkupSafe-2.0.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:d6c7ebd4e944c85e2c3421e612a7057a2f48d478d79e61800d81468a8d842207"}, - {file = "MarkupSafe-2.0.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:f0567c4dc99f264f49fe27da5f735f414c4e7e7dd850cfd8e69f0862d7c74ea9"}, - {file = "MarkupSafe-2.0.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:89c687013cb1cd489a0f0ac24febe8c7a666e6e221b783e53ac50ebf68e45d86"}, {file = "MarkupSafe-2.0.1-cp37-cp37m-win32.whl", hash = "sha256:a30e67a65b53ea0a5e62fe23682cfe22712e01f453b95233b25502f7c61cb415"}, {file = "MarkupSafe-2.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:611d1ad9a4288cf3e3c16014564df047fe08410e628f89805e475368bd304914"}, - {file = "MarkupSafe-2.0.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:5bb28c636d87e840583ee3adeb78172efc47c8b26127267f54a9c0ec251d41a9"}, {file = "MarkupSafe-2.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:be98f628055368795d818ebf93da628541e10b75b41c559fdf36d104c5787066"}, {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:1d609f577dc6e1aa17d746f8bd3c31aa4d258f4070d61b2aa5c4166c1539de35"}, {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:7d91275b0245b1da4d4cfa07e0faedd5b0812efc15b702576d103293e252af1b"}, {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:01a9b8ea66f1658938f65b93a85ebe8bc016e6769611be228d797c9d998dd298"}, {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:47ab1e7b91c098ab893b828deafa1203de86d0bc6ab587b160f78fe6c4011f75"}, {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:97383d78eb34da7e1fa37dd273c20ad4320929af65d156e35a5e2d89566d9dfb"}, - {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6fcf051089389abe060c9cd7caa212c707e58153afa2c649f00346ce6d260f1b"}, - {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:5855f8438a7d1d458206a2466bf82b0f104a3724bf96a1c781ab731e4201731a"}, - {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:3dd007d54ee88b46be476e293f48c85048603f5f516008bee124ddd891398ed6"}, - {file = "MarkupSafe-2.0.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:aca6377c0cb8a8253e493c6b451565ac77e98c2951c45f913e0b52facdcff83f"}, - {file = "MarkupSafe-2.0.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:04635854b943835a6ea959e948d19dcd311762c5c0c6e1f0e16ee57022669194"}, - {file = "MarkupSafe-2.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6300b8454aa6930a24b9618fbb54b5a68135092bc666f7b06901f897fa5c2fee"}, {file = "MarkupSafe-2.0.1-cp38-cp38-win32.whl", hash = "sha256:023cb26ec21ece8dc3907c0e8320058b2e0cb3c55cf9564da612bc325bed5e64"}, {file = "MarkupSafe-2.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:984d76483eb32f1bcb536dc27e4ad56bba4baa70be32fa87152832cdd9db0833"}, {file = "MarkupSafe-2.0.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:2ef54abee730b502252bcdf31b10dacb0a416229b72c18b19e24a4509f273d26"}, @@ -1323,12 +1338,6 @@ markupsafe = [ {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:4efca8f86c54b22348a5467704e3fec767b2db12fc39c6d963168ab1d3fc9135"}, {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:ab3ef638ace319fa26553db0624c4699e31a28bb2a835c5faca8f8acf6a5a902"}, {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:f8ba0e8349a38d3001fae7eadded3f6606f0da5d748ee53cc1dab1d6527b9509"}, - {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c47adbc92fc1bb2b3274c4b3a43ae0e4573d9fbff4f54cd484555edbf030baf1"}, - {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:37205cac2a79194e3750b0af2a5720d95f786a55ce7df90c3af697bfa100eaac"}, - {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:1f2ade76b9903f39aa442b4aadd2177decb66525062db244b35d71d0ee8599b6"}, - {file = "MarkupSafe-2.0.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:4296f2b1ce8c86a6aea78613c34bb1a672ea0e3de9c6ba08a960efe0b0a09047"}, - {file = "MarkupSafe-2.0.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9f02365d4e99430a12647f09b6cc8bab61a6564363f313126f775eb4f6ef798e"}, - {file = "MarkupSafe-2.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5b6d930f030f8ed98e3e6c98ffa0652bdb82601e7a016ec2ab5d7ff23baa78d1"}, {file = "MarkupSafe-2.0.1-cp39-cp39-win32.whl", hash = "sha256:10f82115e21dc0dfec9ab5c0223652f7197feb168c940f3ef61563fc2d6beb74"}, {file = "MarkupSafe-2.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:693ce3f9e70a6cf7d2fb9e6c9d8b204b6b39897a2c4a1aa65728d5ac97dcc1d8"}, {file = "MarkupSafe-2.0.1.tar.gz", hash = "sha256:594c67807fb16238b30c44bdf74f36c02cdf22d1c8cda91ef8a0ed8dabf5620a"}, @@ -1385,6 +1394,22 @@ mypy = [ {file = "mypy-0.950-py3-none-any.whl", hash = "sha256:a4d9898f46446bfb6405383b57b96737dcfd0a7f25b748e78ef3e8c576bba3cb"}, {file = "mypy-0.950.tar.gz", hash = "sha256:1b333cfbca1762ff15808a0ef4f71b5d3eed8528b23ea1c3fb50543c867d68de"}, ] +mypy-boto3-appconfig = [ + {file = "mypy-boto3-appconfig-1.21.34.tar.gz", hash = "sha256:620350b576e7a5df62b30829f5b9760e4aa105e1b7b2455cccb2be32e529e950"}, + {file = "mypy_boto3_appconfig-1.21.34-py3-none-any.whl", hash = "sha256:1c9c20e5c9436f65989e35692497a3c1c19d6b28aeb9d6989cee3b0cfb1b3f7d"}, +] +mypy-boto3-dynamodb = [ + {file = "mypy-boto3-dynamodb-1.23.0.post1.tar.gz", hash = "sha256:4670825645d041881f3f37a70b38e4b771171942808e49a011a63a9ea6cf494c"}, + {file = "mypy_boto3_dynamodb-1.23.0.post1-py3-none-any.whl", hash = "sha256:fed40bd6e987d4dbe2551b2a33106f23965111570e0a84e9e7a3caf65d1c79f9"}, +] +mypy-boto3-secretsmanager = [ + {file = "mypy-boto3-secretsmanager-1.21.34.tar.gz", hash = "sha256:91f517ef0b3e1fb668db873146b8404fd6926c300ba0cef8df847e8929b4b4e5"}, + {file = "mypy_boto3_secretsmanager-1.21.34-py3-none-any.whl", hash = "sha256:534fed05d1a5b5caf7032a88f295d58addf1135905d1559a87109a41e34b3b23"}, +] +mypy-boto3-ssm = [ + {file = "mypy-boto3-ssm-1.21.34.tar.gz", hash = "sha256:cfc59e6c0513a3b72e3036a1785fcb57a1c4563db0e05515b0f082a34e0981c3"}, + {file = "mypy_boto3_ssm-1.21.34-py3-none-any.whl", hash = "sha256:f799b77d611c4d84d2915957423d06de8ee0c270d385374387c7b022f5941b34"}, +] mypy-extensions = [ {file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"}, {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"}, @@ -1402,7 +1427,6 @@ pbr = [ {file = "pbr-5.8.0.tar.gz", hash = "sha256:672d8ebee84921862110f23fcec2acea191ef58543d34dfe9ef3d9f13c31cddf"}, ] pdoc3 = [ - {file = "pdoc3-0.10.0-py3-none-any.whl", hash = "sha256:ba45d1ada1bd987427d2bf5cdec30b2631a3ff5fb01f6d0e77648a572ce6028b"}, {file = "pdoc3-0.10.0.tar.gz", hash = "sha256:5f22e7bcb969006738e1aa4219c75a32f34c2d62d46dc9d2fb2d3e0b0287e4b7"}, ] platformdirs = [ @@ -1629,8 +1653,8 @@ typed-ast = [ {file = "typed_ast-1.4.3.tar.gz", hash = "sha256:fb1bbeac803adea29cedd70781399c99138358c26d05fcbd23c13016b7f5ec65"}, ] typing-extensions = [ - {file = "typing_extensions-4.0.1-py3-none-any.whl", hash = "sha256:7f001e5ac290a0c0401508864c7ec868be4e701886d5b573a9528ed3973d9d3b"}, - {file = "typing_extensions-4.0.1.tar.gz", hash = "sha256:4ca091dea149f945ec56afb48dae714f21e8692ef22a395223bcd328961b6a0e"}, + {file = "typing_extensions-4.1.1-py3-none-any.whl", hash = "sha256:21c85e0fe4b9a155d0799430b0ad741cdce7e359660ccbd8b530613e8df88ce2"}, + {file = "typing_extensions-4.1.1.tar.gz", hash = "sha256:1a9462dcc3347a79b1f1c0271fbe79e844580bb598bafa1ed208b94da3cdcd42"}, ] urllib3 = [ {file = "urllib3-1.26.7-py2.py3-none-any.whl", hash = "sha256:c4fdf4019605b6e5423637e01bc9fe4daef873709a7973e195ceba0a62bbc844"}, diff --git a/pyproject.toml b/pyproject.toml index e90f457bdd0..93ae802d6c6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -55,6 +55,10 @@ mkdocs-git-revision-date-plugin = "^0.3.2" mike = "^0.6.0" mypy = "^0.950" mkdocs-material = "^8.2.7" +mypy-boto3-secretsmanager = "^1.21.34" +mypy-boto3-ssm = "^1.21.34" +mypy-boto3-appconfig = "^1.21.34" +mypy-boto3-dynamodb = "^1.23.0" [tool.poetry.extras] @@ -87,8 +91,11 @@ exclude_lines = [ "if 0:", "if __name__ == .__main__.:", - # Ignore type function overload - "@overload", + # Ignore runtime type checking + "if TYPE_CHECKING:", + + # Ignore type function overload + "@overload", ] [tool.isort] diff --git a/tests/functional/test_utilities_parameters.py b/tests/functional/test_utilities_parameters.py index ba9ee49d924..2b8291db47b 100644 --- a/tests/functional/test_utilities_parameters.py +++ b/tests/functional/test_utilities_parameters.py @@ -6,6 +6,7 @@ from io import BytesIO from typing import Dict +import boto3 import pytest from boto3.dynamodb.conditions import Key from botocore import stub @@ -173,6 +174,36 @@ def test_dynamodb_provider_get_sdk_options(mock_name, mock_value, config): stubber.deactivate() +def test_dynamodb_provider_get_with_custom_client(mock_name, mock_value, config): + """ + Test DynamoDBProvider.get() with SDK options + """ + + table_name = "TEST_TABLE" + client = boto3.resource("dynamodb", config=config) + table_resource_client = client.Table(table_name) + + # Create a new provider + provider = parameters.DynamoDBProvider(table_name, boto3_client=client) + + # Stub the boto3 client + stubber = stub.Stubber(provider.table.meta.client) + response = {"Item": {"id": {"S": mock_name}, "value": {"S": mock_value}}} + expected_params = {"TableName": table_name, "Key": {"id": mock_name}, "ConsistentRead": True} + stubber.add_response("get_item", response, expected_params) + stubber.activate() + + try: + value = provider.get(mock_name, ConsistentRead=True) + + assert value == mock_value + # confirm table resource client comes from the same custom client provided + assert id(table_resource_client.meta.client) == id(provider.table.meta.client) + stubber.assert_no_pending_responses() + finally: + stubber.deactivate() + + def test_dynamodb_provider_get_sdk_options_overwrite(mock_name, mock_value, config): """ Test DynamoDBProvider.get() with SDK options that should be overwritten @@ -444,6 +475,43 @@ def test_ssm_provider_get(mock_name, mock_value, mock_version, config): stubber.deactivate() +def test_ssm_provider_get_with_custom_client(mock_name, mock_value, mock_version, config): + """ + Test SSMProvider.get() with a non-cached value + """ + + client = boto3.client("ssm", config=config) + + # Create a new provider + provider = parameters.SSMProvider(boto3_client=client) + + # Stub the boto3 client + stubber = stub.Stubber(provider.client) + response = { + "Parameter": { + "Name": mock_name, + "Type": "String", + "Value": mock_value, + "Version": mock_version, + "Selector": f"{mock_name}:{mock_version}", + "SourceResult": "string", + "LastModifiedDate": datetime(2015, 1, 1), + "ARN": f"arn:aws:ssm:us-east-2:111122223333:parameter/{mock_name}", + } + } + expected_params = {"Name": mock_name, "WithDecryption": False} + stubber.add_response("get_parameter", response, expected_params) + stubber.activate() + + try: + value = provider.get(mock_name) + + assert value == mock_value + stubber.assert_no_pending_responses() + finally: + stubber.deactivate() + + def test_ssm_provider_get_default_config(monkeypatch, mock_name, mock_value, mock_version): """ Test SSMProvider.get() without specifying the config @@ -925,6 +993,37 @@ def test_secrets_provider_get(mock_name, mock_value, config): stubber.deactivate() +def test_secrets_provider_get_with_custom_client(mock_name, mock_value, config): + """ + Test SecretsProvider.get() with a non-cached value + """ + client = boto3.client("secretsmanager", config=config) + + # Create a new provider + provider = parameters.SecretsProvider(boto3_client=client) + + # Stub the boto3 client + stubber = stub.Stubber(provider.client) + response = { + "ARN": f"arn:aws:secretsmanager:us-east-1:132456789012:secret/{mock_name}", + "Name": mock_name, + "VersionId": "7a9155b8-2dc9-466e-b4f6-5bc46516c84d", + "SecretString": mock_value, + "CreatedDate": datetime(2015, 1, 1), + } + expected_params = {"SecretId": mock_name} + stubber.add_response("get_secret_value", response, expected_params) + stubber.activate() + + try: + value = provider.get(mock_name) + + assert value == mock_value + stubber.assert_no_pending_responses() + finally: + stubber.deactivate() + + def test_secrets_provider_get_default_config(monkeypatch, mock_name, mock_value): """ Test SecretsProvider.get() without specifying a config @@ -1555,6 +1654,37 @@ def test_appconf_provider_get_configuration_json_content_type(mock_name, config) stubber.deactivate() +def test_appconf_provider_get_configuration_json_content_type_with_custom_client(mock_name, config): + """ + Test get_configuration.get with default values + """ + + client = boto3.client("appconfig", config=config) + + # Create a new provider + environment = "dev" + application = "myapp" + provider = parameters.AppConfigProvider(environment=environment, application=application, boto3_client=client) + + mock_body_json = {"myenvvar1": "Black Panther", "myenvvar2": 3} + encoded_message = json.dumps(mock_body_json).encode("utf-8") + mock_value = StreamingBody(BytesIO(encoded_message), len(encoded_message)) + + # Stub the boto3 client + stubber = stub.Stubber(provider.client) + response = {"Content": mock_value, "ConfigurationVersion": "1", "ContentType": "application/json"} + stubber.add_response("get_configuration", response) + stubber.activate() + + try: + value = provider.get(mock_name, transform="json", ClientConfigurationVersion="2") + + assert value == mock_body_json + stubber.assert_no_pending_responses() + finally: + stubber.deactivate() + + def test_appconf_provider_get_configuration_no_transform(mock_name, config): """ Test appconfigprovider.get with default values