Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 3 additions & 4 deletions aws_lambda_powertools/utilities/parameters/appconfig.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,9 @@
from aws_lambda_powertools.shared import constants
from aws_lambda_powertools.shared.functions import (
resolve_env_var_choice,
resolve_max_age,
)
from aws_lambda_powertools.utilities.parameters.base import BaseProvider
from aws_lambda_powertools.utilities.parameters.constants import DEFAULT_MAX_AGE_SECS, DEFAULT_PROVIDERS
from aws_lambda_powertools.utilities.parameters.base import BaseProvider, resolve_parameters_max_age
from aws_lambda_powertools.utilities.parameters.constants import DEFAULT_PROVIDERS
from aws_lambda_powertools.utilities.parameters.exceptions import GetParameterError
from aws_lambda_powertools.warnings import PowertoolsDeprecationWarning

Expand Down Expand Up @@ -262,7 +261,7 @@ def get_app_config(
My configuration's JSON value
"""
# If max_age is not set, resolve it from the environment variable, defaulting to DEFAULT_MAX_AGE_SECS
max_age = resolve_max_age(env=os.getenv(constants.PARAMETERS_MAX_AGE_ENV, DEFAULT_MAX_AGE_SECS), choice=max_age)
max_age = resolve_parameters_max_age(choice=max_age)

# One provider per environment/application pair, created on first use
provider_key = f"appconfig:{environment}:{application}"
Expand Down
42 changes: 39 additions & 3 deletions aws_lambda_powertools/utilities/parameters/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,15 @@

import json
import os
import warnings
from abc import ABC, abstractmethod
from collections.abc import Callable
from datetime import datetime, timedelta
from typing import TYPE_CHECKING, Any, NamedTuple, cast, overload

from aws_lambda_powertools.shared import constants, user_agent
from aws_lambda_powertools.shared.functions import resolve_max_age
from aws_lambda_powertools.utilities.parameters.exceptions import GetParameterError, TransformParameterError
from aws_lambda_powertools.warnings import PowertoolsUserWarning

if TYPE_CHECKING:
from aws_lambda_powertools.utilities.parameters.types import TransformOptions
Expand Down Expand Up @@ -102,7 +103,7 @@ def get(
key = self._build_cache_key(name=name, transform=transform, sdk_options=sdk_options)

# If max_age is not set, resolve it from the environment variable, defaulting to DEFAULT_MAX_AGE_SECS
max_age = resolve_max_age(env=os.getenv(constants.PARAMETERS_MAX_AGE_ENV, DEFAULT_MAX_AGE_SECS), choice=max_age)
max_age = resolve_parameters_max_age(choice=max_age)

if not force_fetch and self.has_not_expired_in_cache(key):
return self.fetch_from_cache(key)
Expand Down Expand Up @@ -176,7 +177,7 @@ def get_multiple(
key = self._build_cache_key(name=path, transform=transform, is_nested=True, sdk_options=sdk_options)

# If max_age is not set, resolve it from the environment variable, defaulting to DEFAULT_MAX_AGE_SECS
max_age = resolve_max_age(env=os.getenv(constants.PARAMETERS_MAX_AGE_ENV, DEFAULT_MAX_AGE_SECS), choice=max_age)
max_age = resolve_parameters_max_age(choice=max_age)

if not force_fetch and self.has_not_expired_in_cache(key):
return self.fetch_from_cache(key)
Expand Down Expand Up @@ -272,6 +273,41 @@ def _build_cache_key(
return (name, transform, is_nested, json.dumps(sdk_options, sort_keys=True, default=str))


def resolve_parameters_max_age(choice: int | None) -> int:
"""
Resolve the caching max age against the ``POWERTOOLS_PARAMETERS_MAX_AGE`` environment variable.

An explicit ``choice`` always wins. Otherwise the environment variable is used, and an
empty or non-integer value is ignored with a :class:`PowertoolsUserWarning` rather than
crashing every lookup: a configuration typo must not break parameter and secret reads
(ADR 0006: warn now, harden in a future major).

Parameters
----------
choice : int | None
Explicit max age passed by the caller, taking precedence over the environment.

Returns
-------
int
The resolved max age in seconds.
"""
if choice is not None:
return choice

env = os.getenv(constants.PARAMETERS_MAX_AGE_ENV, DEFAULT_MAX_AGE_SECS)
try:
return int(env)
except ValueError:
warnings.warn(
f"Ignoring invalid {constants.PARAMETERS_MAX_AGE_ENV} value {env!r}; "
f"falling back to the default of {DEFAULT_MAX_AGE_SECS} seconds.",

@Manishram-ai Manishram-ai Nov 5, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The docstring above says this hardens in a future major. If that's the plan, ADR 0006 asks for the warning itself to say what will change and when, so the message could mention that it becomes an error in the next major. If the fallback is meant to stay, the "harden in a future major" part of the docstring could go instead.

category=PowertoolsUserWarning,
stacklevel=3,
)
return int(DEFAULT_MAX_AGE_SECS)


def get_transform_method(value: str, transform: TransformOptions = None) -> Callable[..., Any]:
"""
Determine the transform method
Expand Down
13 changes: 5 additions & 8 deletions aws_lambda_powertools/utilities/parameters/secrets.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,14 @@

import json
import logging
import os
import warnings
from typing import TYPE_CHECKING, Any, Literal, overload

import boto3

from aws_lambda_powertools.shared import constants
from aws_lambda_powertools.shared.functions import resolve_max_age
from aws_lambda_powertools.shared.json_encoder import Encoder
from aws_lambda_powertools.utilities.parameters.base import BaseProvider, transform_value
from aws_lambda_powertools.utilities.parameters.constants import DEFAULT_MAX_AGE_SECS, DEFAULT_PROVIDERS
from aws_lambda_powertools.utilities.parameters.base import BaseProvider, resolve_parameters_max_age, transform_value
from aws_lambda_powertools.utilities.parameters.constants import DEFAULT_PROVIDERS
from aws_lambda_powertools.utilities.parameters.exceptions import (
GetSecretError,
SetSecretError,
Expand Down Expand Up @@ -256,7 +253,7 @@ def get_multiple( # type: ignore[override] # ty: ignore[invalid-method-overrid
)

# If max_age is not set, resolve it from the environment variable, defaulting to DEFAULT_MAX_AGE_SECS
max_age = resolve_max_age(env=os.getenv(constants.PARAMETERS_MAX_AGE_ENV, DEFAULT_MAX_AGE_SECS), choice=max_age)
max_age = resolve_parameters_max_age(choice=max_age)

if not force_fetch and self.has_not_expired_in_cache(key):
cached_values = self.fetch_from_cache(key)
Expand Down Expand Up @@ -517,7 +514,7 @@ def get_secret(
"""

# If max_age is not set, resolve it from the environment variable, defaulting to DEFAULT_MAX_AGE_SECS
max_age = resolve_max_age(env=os.getenv(constants.PARAMETERS_MAX_AGE_ENV, DEFAULT_MAX_AGE_SECS), choice=max_age)
max_age = resolve_parameters_max_age(choice=max_age)

# Only create the provider if this function is called at least once
if "secrets" not in DEFAULT_PROVIDERS:
Expand Down Expand Up @@ -596,7 +593,7 @@ def get_secrets_by_name(
raise GetSecretError("You must provide at least one secret name")

# If max_age is not set, resolve it from the environment variable, defaulting to DEFAULT_MAX_AGE_SECS
max_age = resolve_max_age(env=os.getenv(constants.PARAMETERS_MAX_AGE_ENV, DEFAULT_MAX_AGE_SECS), choice=max_age)
max_age = resolve_parameters_max_age(choice=max_age)

# Only create the provider if this function is called at least once
if "secrets" not in DEFAULT_PROVIDERS:
Expand Down
15 changes: 7 additions & 8 deletions aws_lambda_powertools/utilities/parameters/ssm.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,15 @@

from aws_lambda_powertools.shared import constants
from aws_lambda_powertools.shared.functions import (
resolve_max_age,
resolve_truthy_env_var_choice,
slice_dictionary,
)
from aws_lambda_powertools.utilities.parameters.base import (
BaseProvider,
resolve_parameters_max_age,
transform_value,
)
from aws_lambda_powertools.utilities.parameters.constants import (
DEFAULT_MAX_AGE_SECS,
DEFAULT_PROVIDERS,
SSM_PARAMETER_TIER,
SSM_PARAMETER_TYPES,
Expand Down Expand Up @@ -181,7 +180,7 @@ def get_multiple( # type: ignore[override] # ty: ignore[invalid-method-overrid
"""

# If max_age is not set, resolve it from the environment variable, defaulting to DEFAULT_MAX_AGE_SECS
max_age = resolve_max_age(env=os.getenv(constants.PARAMETERS_MAX_AGE_ENV, DEFAULT_MAX_AGE_SECS), choice=max_age)
max_age = resolve_parameters_max_age(choice=max_age)

# If decrypt is not set, resolve it from the environment variable, defaulting to False
decrypt = resolve_truthy_env_var_choice(
Expand Down Expand Up @@ -235,7 +234,7 @@ def get( # type: ignore[override] # ty: ignore[invalid-method-override]
"""

# If max_age is not set, resolve it from the environment variable, defaulting to DEFAULT_MAX_AGE_SECS
max_age = resolve_max_age(env=os.getenv(constants.PARAMETERS_MAX_AGE_ENV, DEFAULT_MAX_AGE_SECS), choice=max_age)
max_age = resolve_parameters_max_age(choice=max_age)

# If decrypt is not set, resolve it from the environment variable, defaulting to False
decrypt = resolve_truthy_env_var_choice(
Expand Down Expand Up @@ -490,7 +489,7 @@ def get_parameters_by_name(
"""

# If max_age is not set, resolve it from the environment variable, defaulting to DEFAULT_MAX_AGE_SECS
max_age = resolve_max_age(env=os.getenv(constants.PARAMETERS_MAX_AGE_ENV, DEFAULT_MAX_AGE_SECS), choice=max_age)
max_age = resolve_parameters_max_age(choice=max_age)

# If decrypt is not set, resolve it from the environment variable, defaulting to False
decrypt = resolve_truthy_env_var_choice(
Expand Down Expand Up @@ -881,7 +880,7 @@ def get_parameter(
DEFAULT_PROVIDERS["ssm"] = SSMProvider()

# If max_age is not set, resolve it from the environment variable, defaulting to DEFAULT_MAX_AGE_SECS
max_age = resolve_max_age(env=os.getenv(constants.PARAMETERS_MAX_AGE_ENV, DEFAULT_MAX_AGE_SECS), choice=max_age)
max_age = resolve_parameters_max_age(choice=max_age)

# If decrypt is not set, resolve it from the environment variable, defaulting to False
decrypt = resolve_truthy_env_var_choice(
Expand Down Expand Up @@ -1019,7 +1018,7 @@ def get_parameters(
DEFAULT_PROVIDERS["ssm"] = SSMProvider()

# If max_age is not set, resolve it from the environment variable, defaulting to DEFAULT_MAX_AGE_SECS
max_age = resolve_max_age(env=os.getenv(constants.PARAMETERS_MAX_AGE_ENV, DEFAULT_MAX_AGE_SECS), choice=max_age)
max_age = resolve_parameters_max_age(choice=max_age)

# If decrypt is not set, resolve it from the environment variable, defaulting to False
decrypt = resolve_truthy_env_var_choice(
Expand Down Expand Up @@ -1215,7 +1214,7 @@ def get_parameters_by_name(
# see: https://github.com/aws-powertools/powertools-lambda-python/issues/1040#issuecomment-1299954613

# If max_age is not set, resolve it from the environment variable, defaulting to DEFAULT_MAX_AGE_SECS
max_age = resolve_max_age(env=os.getenv(constants.PARAMETERS_MAX_AGE_ENV, DEFAULT_MAX_AGE_SECS), choice=max_age)
max_age = resolve_parameters_max_age(choice=max_age)

# If decrypt is not set, resolve it from the environment variable, defaulting to False
decrypt = resolve_truthy_env_var_choice(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
from botocore.config import Config
from botocore.response import StreamingBody

from aws_lambda_powertools.shared import constants
from aws_lambda_powertools.utilities import parameters
from aws_lambda_powertools.utilities.parameters import AppConfigProvider, DynamoDBProvider, SecretsProvider, SSMProvider
from aws_lambda_powertools.utilities.parameters.base import (
Expand All @@ -24,7 +25,7 @@
ExpirableValue,
)
from aws_lambda_powertools.utilities.parameters.exceptions import GetSecretError
from aws_lambda_powertools.warnings import PowertoolsDeprecationWarning
from aws_lambda_powertools.warnings import PowertoolsDeprecationWarning, PowertoolsUserWarning


@pytest.fixture(scope="function")
Expand Down Expand Up @@ -572,6 +573,51 @@ def test_ssm_provider_get(mock_name, mock_value, mock_version, config):
stubber.deactivate()


def test_ssm_provider_get_with_invalid_max_age_env_var(mock_name, mock_value, mock_version, config, monkeypatch):
"""
Test SSMProvider.get() when POWERTOOLS_PARAMETERS_MAX_AGE is empty or non-integer.

A malformed environment variable must not break every parameters call: the
documented default applies and a PowertoolsUserWarning names the ignored value.
"""

# Create a new provider
provider = parameters.SSMProvider(boto_config=config)

# Stub the boto3 client; one SDK call per env var value tested
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.add_response("get_parameter", response, expected_params)
stubber.activate()

try:
for env_value in ("", "abc"):
monkeypatch.setenv(constants.PARAMETERS_MAX_AGE_ENV, env_value)
# Each iteration resolves the env var afresh: the default applied
# after the first lookup would otherwise serve the second from cache.
provider.clear_cache()
with pytest.warns(PowertoolsUserWarning, match=constants.PARAMETERS_MAX_AGE_ENV):
value = provider.get(mock_name)
assert value == mock_value

stubber.assert_no_pending_responses()
finally:
stubber.deactivate()


def test_set_parameter(monkeypatch, mock_name, mock_value):
"""
Test set_parameter()
Expand Down