diff --git a/botocore/endpoint_provider.py b/botocore/endpoint_provider.py index 0fbd802b8d..d76f9ac245 100644 --- a/botocore/endpoint_provider.py +++ b/botocore/endpoint_provider.py @@ -577,6 +577,7 @@ class ParameterType(Enum): string = str boolean = bool + stringarray = tuple class ParameterDefinition: @@ -600,7 +601,7 @@ def __init__( except AttributeError: raise EndpointResolutionError( msg=f"Unknown parameter type: {parameter_type}. " - "A parameter must be of type string or boolean." + "A parameter must be of type string, boolean, or stringarray." ) self.documentation = documentation self.builtin = builtIn diff --git a/botocore/model.py b/botocore/model.py index 70d20f8cca..efd15e90e7 100644 --- a/botocore/model.py +++ b/botocore/model.py @@ -624,6 +624,10 @@ def context_parameters(self): and 'name' in shape.metadata['contextParam'] ] + @CachedProperty + def operation_context_parameters(self): + return self._operation_model.get('operationContextParams', []) + @CachedProperty def request_compression(self): return self._operation_model.get('requestcompression') diff --git a/botocore/regions.py b/botocore/regions.py index 4c23737270..d3b2705727 100644 --- a/botocore/regions.py +++ b/botocore/regions.py @@ -22,6 +22,8 @@ import re from enum import Enum +import jmespath + from botocore import UNSIGNED, xform_name from botocore.auth import AUTH_TYPE_MAPS, HAS_CRT from botocore.crt import CRT_SUPPORTED_AUTH_TYPES @@ -578,6 +580,13 @@ def _resolve_param_from_context( ) if dynamic is not None: return dynamic + operation_context_params = ( + self._resolve_param_as_operation_context_param( + param_name, operation_model, call_args + ) + ) + if operation_context_params is not None: + return operation_context_params return self._resolve_param_as_client_context_param(param_name) def _resolve_param_as_static_context_param( @@ -600,6 +609,14 @@ def _resolve_param_as_client_context_param(self, param_name): client_ctx_varname = client_ctx_params[param_name] return self._client_context.get(client_ctx_varname) + def _resolve_param_as_operation_context_param( + self, param_name, operation_model, call_args + ): + operation_ctx_params = operation_model.operation_context_parameters + if param_name in operation_ctx_params: + path = operation_ctx_params[param_name]['path'] + return jmespath.search(path, call_args) + def _resolve_param_as_builtin(self, builtin_name, builtins): if builtin_name not in EndpointResolverBuiltins.__members__.values(): raise UnknownEndpointResolutionBuiltInName(name=builtin_name) diff --git a/botocore/utils.py b/botocore/utils.py index a54f4a39d3..2d55e8a8dc 100644 --- a/botocore/utils.py +++ b/botocore/utils.py @@ -1479,7 +1479,7 @@ def lru_cache_weakref(*cache_args, **cache_kwargs): functools implementation which offers ``max_size`` and ``typed`` properties. lru_cache is a global cache even when used on a method. The cache's - reference to ``self`` will prevent garbace collection of the object. This + reference to ``self`` will prevent garbage collection of the object. This wrapper around functools.lru_cache replaces the reference to ``self`` with a weak reference to not interfere with garbage collection. """ @@ -1491,6 +1491,9 @@ def func_with_weakref(weakref_to_self, *args, **kwargs): @functools.wraps(func) def inner(self, *args, **kwargs): + for kwarg_key, kwarg_value in kwargs.items(): + if isinstance(kwarg_value, list): + kwargs[kwarg_key] = tuple(kwarg_value) return func_with_weakref(weakref.ref(self), *args, **kwargs) inner.cache_info = func_with_weakref.cache_info diff --git a/tests/unit/data/endpoints/test-cases/array-index.json b/tests/unit/data/endpoints/test-cases/array-index.json new file mode 100644 index 0000000000..aa73d460d3 --- /dev/null +++ b/tests/unit/data/endpoints/test-cases/array-index.json @@ -0,0 +1,37 @@ +{ + "version": "1.0", + "testCases": [ + { + "documentation": "Access an array index at index 0", + "params": { + "ResourceList": ["resource"] + }, + "expect": { + "endpoint": { + "url": "https://www.resource.example.com" + } + } + }, + { + "documentation": "Resolved value when array is explictly set to empty", + "params": { + "ResourceList": [] + }, + "expect": { + "endpoint": { + "url": "https://www.example.com" + } + } + }, + { + "documentation": "Resolved value to default if array is unset", + "params": { + }, + "expect": { + "endpoint": { + "url": "https://www.default1.example.com" + } + } + } + ] +} diff --git a/tests/unit/data/endpoints/valid-rules/array-index.json b/tests/unit/data/endpoints/valid-rules/array-index.json new file mode 100644 index 0000000000..3d426b09aa --- /dev/null +++ b/tests/unit/data/endpoints/valid-rules/array-index.json @@ -0,0 +1,47 @@ +{ + "version": "1.3", + "parameters": { + "ResourceList": { + "required": true, + "default": ["default1", "default2"], + "type": "stringArray" + } + }, + "rules": [ + { + "documentation": "Array is set, retrieve index 0", + "conditions": [ + { + "fn": "isSet", + "argv": [ + { + "ref": "ResourceList" + } + ] + }, + { + "fn": "getAttr", + "argv": [ + { + "ref": "ResourceList" + }, + "[0]" + ], + "assign": "resourceid" + } + ], + "endpoint": { + "url": "https://www.{resourceid}.example.com" + }, + "type": "endpoint" + }, + { + "documentation": "Fallback when array is unset", + "conditions": [], + "endpoint": { + "url": "https://www.example.com" + }, + "type": "endpoint" + } + ] +} diff --git a/tests/unit/test_endpoint_provider.py b/tests/unit/test_endpoint_provider.py index 8bc8c429d0..3d6bf81e32 100644 --- a/tests/unit/test_endpoint_provider.py +++ b/tests/unit/test_endpoint_provider.py @@ -137,6 +137,7 @@ def endpoint_rule(): def ruleset_testcases(): filenames = [ + "array-index", "aws-region", "default-values", "eventbridge",