You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
344 lines
13 KiB
344 lines
13 KiB
6 months ago
|
# 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<ref_valid_dynamodb_types>`.'
|
||
|
),
|
||
|
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<ref_dynamodb_conditions>`.'
|
||
|
),
|
||
|
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<ref_dynamodb_conditions>`.'
|
||
|
),
|
||
|
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
|
||
|
)
|