# Copyright 2015 Amazon.com, Inc. or its affiliates. All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"). You # may not use this file except in compliance with the License. A copy of # the License is located at # # https://aws.amazon.com/apache2.0/ # # or in the "license" file accompanying this file. This file is # distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF # ANY KIND, either express or implied. See the License for the specific # language governing permissions and limitations under the License. import copy from boto3.compat import collections_abc from boto3.docs.utils import DocumentModifiedShape from boto3.dynamodb.conditions import ConditionBase, ConditionExpressionBuilder from boto3.dynamodb.types import TypeDeserializer, TypeSerializer def register_high_level_interface(base_classes, **kwargs): base_classes.insert(0, DynamoDBHighLevelResource) class _ForgetfulDict(dict): """A dictionary that discards any items set on it. For use as `memo` in `copy.deepcopy()` when every instance of a repeated object in the deepcopied data structure should result in a separate copy. """ def __setitem__(self, key, value): pass def copy_dynamodb_params(params, **kwargs): return copy.deepcopy(params, memo=_ForgetfulDict()) class DynamoDBHighLevelResource: def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) # Apply handler that creates a copy of the user provided dynamodb # item such that it can be modified. self.meta.client.meta.events.register( 'provide-client-params.dynamodb', copy_dynamodb_params, unique_id='dynamodb-create-params-copy', ) self._injector = TransformationInjector() # Apply the handler that generates condition expressions including # placeholders. self.meta.client.meta.events.register( 'before-parameter-build.dynamodb', self._injector.inject_condition_expressions, unique_id='dynamodb-condition-expression', ) # Apply the handler that serializes the request from python # types to dynamodb types. self.meta.client.meta.events.register( 'before-parameter-build.dynamodb', self._injector.inject_attribute_value_input, unique_id='dynamodb-attr-value-input', ) # Apply the handler that deserializes the response from dynamodb # types to python types. self.meta.client.meta.events.register( 'after-call.dynamodb', self._injector.inject_attribute_value_output, unique_id='dynamodb-attr-value-output', ) # Apply the documentation customizations to account for # the transformations. attr_value_shape_docs = DocumentModifiedShape( 'AttributeValue', new_type='valid DynamoDB type', new_description=( '- The value of the attribute. The valid value types are ' 'listed in the ' ':ref:`DynamoDB Reference Guide`.' ), new_example_value=( '\'string\'|123|Binary(b\'bytes\')|True|None|set([\'string\'])' '|set([123])|set([Binary(b\'bytes\')])|[]|{}' ), ) key_expression_shape_docs = DocumentModifiedShape( 'KeyExpression', new_type=( 'condition from :py:class:`boto3.dynamodb.conditions.Key` ' 'method' ), new_description=( 'The condition(s) a key(s) must meet. Valid conditions are ' 'listed in the ' ':ref:`DynamoDB Reference Guide`.' ), new_example_value='Key(\'mykey\').eq(\'myvalue\')', ) con_expression_shape_docs = DocumentModifiedShape( 'ConditionExpression', new_type=( 'condition from :py:class:`boto3.dynamodb.conditions.Attr` ' 'method' ), new_description=( 'The condition(s) an attribute(s) must meet. Valid conditions ' 'are listed in the ' ':ref:`DynamoDB Reference Guide`.' ), new_example_value='Attr(\'myattribute\').eq(\'myvalue\')', ) self.meta.client.meta.events.register( 'docs.*.dynamodb.*.complete-section', attr_value_shape_docs.replace_documentation_for_matching_shape, unique_id='dynamodb-attr-value-docs', ) self.meta.client.meta.events.register( 'docs.*.dynamodb.*.complete-section', key_expression_shape_docs.replace_documentation_for_matching_shape, unique_id='dynamodb-key-expression-docs', ) self.meta.client.meta.events.register( 'docs.*.dynamodb.*.complete-section', con_expression_shape_docs.replace_documentation_for_matching_shape, unique_id='dynamodb-cond-expression-docs', ) class TransformationInjector: """Injects the transformations into the user provided parameters.""" def __init__( self, transformer=None, condition_builder=None, serializer=None, deserializer=None, ): self._transformer = transformer if transformer is None: self._transformer = ParameterTransformer() self._condition_builder = condition_builder if condition_builder is None: self._condition_builder = ConditionExpressionBuilder() self._serializer = serializer if serializer is None: self._serializer = TypeSerializer() self._deserializer = deserializer if deserializer is None: self._deserializer = TypeDeserializer() def inject_condition_expressions(self, params, model, **kwargs): """Injects the condition expression transformation into the parameters This injection includes transformations for ConditionExpression shapes and KeyExpression shapes. It also handles any placeholder names and values that are generated when transforming the condition expressions. """ self._condition_builder.reset() generated_names = {} generated_values = {} # Create and apply the Condition Expression transformation. transformation = ConditionExpressionTransformation( self._condition_builder, placeholder_names=generated_names, placeholder_values=generated_values, is_key_condition=False, ) self._transformer.transform( params, model.input_shape, transformation, 'ConditionExpression' ) # Create and apply the Key Condition Expression transformation. transformation = ConditionExpressionTransformation( self._condition_builder, placeholder_names=generated_names, placeholder_values=generated_values, is_key_condition=True, ) self._transformer.transform( params, model.input_shape, transformation, 'KeyExpression' ) expr_attr_names_input = 'ExpressionAttributeNames' expr_attr_values_input = 'ExpressionAttributeValues' # Now that all of the condition expression transformation are done, # update the placeholder dictionaries in the request. if expr_attr_names_input in params: params[expr_attr_names_input].update(generated_names) else: if generated_names: params[expr_attr_names_input] = generated_names if expr_attr_values_input in params: params[expr_attr_values_input].update(generated_values) else: if generated_values: params[expr_attr_values_input] = generated_values def inject_attribute_value_input(self, params, model, **kwargs): """Injects DynamoDB serialization into parameter input""" self._transformer.transform( params, model.input_shape, self._serializer.serialize, 'AttributeValue', ) def inject_attribute_value_output(self, parsed, model, **kwargs): """Injects DynamoDB deserialization into responses""" if model.output_shape is not None: self._transformer.transform( parsed, model.output_shape, self._deserializer.deserialize, 'AttributeValue', ) class ConditionExpressionTransformation: """Provides a transformation for condition expressions The ``ParameterTransformer`` class can call this class directly to transform the condition expressions in the parameters provided. """ def __init__( self, condition_builder, placeholder_names, placeholder_values, is_key_condition=False, ): self._condition_builder = condition_builder self._placeholder_names = placeholder_names self._placeholder_values = placeholder_values self._is_key_condition = is_key_condition def __call__(self, value): if isinstance(value, ConditionBase): # Create a conditional expression string with placeholders # for the provided condition. built_expression = self._condition_builder.build_expression( value, is_key_condition=self._is_key_condition ) self._placeholder_names.update( built_expression.attribute_name_placeholders ) self._placeholder_values.update( built_expression.attribute_value_placeholders ) return built_expression.condition_expression # Use the user provided value if it is not a ConditonBase object. return value class ParameterTransformer: """Transforms the input to and output from botocore based on shape""" def transform(self, params, model, transformation, target_shape): """Transforms the dynamodb input to or output from botocore It applies a specified transformation whenever a specific shape name is encountered while traversing the parameters in the dictionary. :param params: The parameters structure to transform. :param model: The operation model. :param transformation: The function to apply the parameter :param target_shape: The name of the shape to apply the transformation to """ self._transform_parameters(model, params, transformation, target_shape) def _transform_parameters( self, model, params, transformation, target_shape ): type_name = model.type_name if type_name in ('structure', 'map', 'list'): getattr(self, f'_transform_{type_name}')( model, params, transformation, target_shape ) def _transform_structure( self, model, params, transformation, target_shape ): if not isinstance(params, collections_abc.Mapping): return for param in params: if param in model.members: member_model = model.members[param] member_shape = member_model.name if member_shape == target_shape: params[param] = transformation(params[param]) else: self._transform_parameters( member_model, params[param], transformation, target_shape, ) def _transform_map(self, model, params, transformation, target_shape): if not isinstance(params, collections_abc.Mapping): return value_model = model.value value_shape = value_model.name for key, value in params.items(): if value_shape == target_shape: params[key] = transformation(value) else: self._transform_parameters( value_model, params[key], transformation, target_shape ) def _transform_list(self, model, params, transformation, target_shape): if not isinstance(params, collections_abc.MutableSequence): return member_model = model.member member_shape = member_model.name for i, item in enumerate(params): if member_shape == target_shape: params[i] = transformation(item) else: self._transform_parameters( member_model, params[i], transformation, target_shape )