6
6
import logging
7
7
import math
8
8
import random
9
+ import re
9
10
import time
10
11
import uuid
11
12
from base64 import b64decode
26
27
COMPARISON_OPERATOR , EXCLUSIVE_START_KEY , SCAN_INDEX_FORWARD , SCAN_FILTER_VALUES , ATTR_DEFINITIONS ,
27
28
BATCH_WRITE_ITEM , CONSISTENT_READ , ATTR_VALUE_LIST , DESCRIBE_TABLE , KEY_CONDITIONS ,
28
29
BATCH_GET_ITEM , DELETE_REQUEST , SELECT_VALUES , RETURN_VALUES , REQUEST_ITEMS , ATTR_UPDATES ,
29
- ATTRS_TO_GET , SERVICE_NAME , DELETE_ITEM , PUT_REQUEST , UPDATE_ITEM , SCAN_FILTER , TABLE_NAME ,
30
+ PROJECTION_EXPRESSION , SERVICE_NAME , DELETE_ITEM , PUT_REQUEST , UPDATE_ITEM , SCAN_FILTER , TABLE_NAME ,
30
31
INDEX_NAME , KEY_SCHEMA , ATTR_NAME , ATTR_TYPE , TABLE_KEY , EXPECTED , KEY_TYPE , GET_ITEM , UPDATE ,
31
32
PUT_ITEM , SELECT , ACTION , EXISTS , VALUE , LIMIT , QUERY , SCAN , ITEM , LOCAL_SECONDARY_INDEXES ,
32
33
KEYS , KEY , EQ , SEGMENT , TOTAL_SEGMENTS , CREATE_TABLE , PROVISIONED_THROUGHPUT , READ_CAPACITY_UNITS ,
35
36
CONSUMED_CAPACITY , CAPACITY_UNITS , QUERY_FILTER , QUERY_FILTER_VALUES , CONDITIONAL_OPERATOR ,
36
37
CONDITIONAL_OPERATORS , NULL , NOT_NULL , SHORT_ATTR_TYPES , DELETE ,
37
38
ITEMS , DEFAULT_ENCODING , BINARY_SHORT , BINARY_SET_SHORT , LAST_EVALUATED_KEY , RESPONSES , UNPROCESSED_KEYS ,
38
- UNPROCESSED_ITEMS , STREAM_SPECIFICATION , STREAM_VIEW_TYPE , STREAM_ENABLED )
39
+ UNPROCESSED_ITEMS , STREAM_SPECIFICATION , STREAM_VIEW_TYPE , STREAM_ENABLED , EXPRESSION_ATTRIBUTE_NAMES )
39
40
from pynamodb .exceptions import (
40
41
TableError , QueryError , PutError , DeleteError , UpdateError , GetError , ScanError , TableDoesNotExist ,
41
42
VerboseClientError
45
46
from pynamodb .types import HASH , RANGE
46
47
47
48
BOTOCORE_EXCEPTIONS = (BotoCoreError , ClientError )
49
+ PATH_SEGMENT_REGEX = re .compile (r'([^\[\]]+)((?:\[\d+\])*)$' )
48
50
49
51
log = logging .getLogger (__name__ )
50
52
log .addHandler (NullHandler ())
@@ -929,12 +931,16 @@ def batch_get_item(self,
929
931
}
930
932
931
933
args_map = {}
934
+ name_placeholders = {}
932
935
if consistent_read :
933
936
args_map [CONSISTENT_READ ] = consistent_read
934
937
if return_consumed_capacity :
935
938
operation_kwargs .update (self .get_consumed_capacity_map (return_consumed_capacity ))
936
939
if attributes_to_get is not None :
937
- args_map [ATTRS_TO_GET ] = attributes_to_get
940
+ projection_expression = self ._get_projection_expression (attributes_to_get , name_placeholders )
941
+ args_map [PROJECTION_EXPRESSION ] = projection_expression
942
+ if name_placeholders :
943
+ args_map [EXPRESSION_ATTRIBUTE_NAMES ] = self ._reverse_dict (name_placeholders )
938
944
operation_kwargs [REQUEST_ITEMS ][table_name ].update (args_map )
939
945
940
946
keys_map = {KEYS : []}
@@ -958,8 +964,12 @@ def get_item(self,
958
964
Performs the GetItem operation and returns the result
959
965
"""
960
966
operation_kwargs = {}
967
+ name_placeholders = {}
961
968
if attributes_to_get is not None :
962
- operation_kwargs [ATTRS_TO_GET ] = attributes_to_get
969
+ projection_expression = self ._get_projection_expression (attributes_to_get , name_placeholders )
970
+ operation_kwargs [PROJECTION_EXPRESSION ] = projection_expression
971
+ if name_placeholders :
972
+ operation_kwargs [EXPRESSION_ATTRIBUTE_NAMES ] = self ._reverse_dict (name_placeholders )
963
973
operation_kwargs [CONSISTENT_READ ] = consistent_read
964
974
operation_kwargs [TABLE_NAME ] = table_name
965
975
operation_kwargs .update (self .get_identifier_map (table_name , hash_key , range_key ))
@@ -1129,8 +1139,12 @@ def scan(self,
1129
1139
Performs the scan operation
1130
1140
"""
1131
1141
operation_kwargs = {TABLE_NAME : table_name }
1142
+ name_placeholders = {}
1132
1143
if attributes_to_get is not None :
1133
- operation_kwargs [ATTRS_TO_GET ] = attributes_to_get
1144
+ projection_expression = self ._get_projection_expression (attributes_to_get , name_placeholders )
1145
+ operation_kwargs [PROJECTION_EXPRESSION ] = projection_expression
1146
+ if name_placeholders :
1147
+ operation_kwargs [EXPRESSION_ATTRIBUTE_NAMES ] = self ._reverse_dict (name_placeholders )
1134
1148
if limit is not None :
1135
1149
operation_kwargs [LIMIT ] = limit
1136
1150
if return_consumed_capacity :
@@ -1183,8 +1197,12 @@ def query(self,
1183
1197
Performs the Query operation and returns the result
1184
1198
"""
1185
1199
operation_kwargs = {TABLE_NAME : table_name }
1200
+ name_placeholders = {}
1186
1201
if attributes_to_get :
1187
- operation_kwargs [ATTRS_TO_GET ] = attributes_to_get
1202
+ projection_expression = self ._get_projection_expression (attributes_to_get , name_placeholders )
1203
+ operation_kwargs [PROJECTION_EXPRESSION ] = projection_expression
1204
+ if name_placeholders :
1205
+ operation_kwargs [EXPRESSION_ATTRIBUTE_NAMES ] = self ._reverse_dict (name_placeholders )
1188
1206
if consistent_read :
1189
1207
operation_kwargs [CONSISTENT_READ ] = True
1190
1208
if exclusive_start_key :
@@ -1244,6 +1262,15 @@ def query(self,
1244
1262
except BOTOCORE_EXCEPTIONS as e :
1245
1263
raise QueryError ("Failed to query items: {0}" .format (e ), e )
1246
1264
1265
+ @staticmethod
1266
+ def _get_projection_expression (attributes_to_get , placeholders ):
1267
+ expressions = [_substitute_names (attribute , placeholders ) for attribute in attributes_to_get ]
1268
+ return ', ' .join (expressions )
1269
+
1270
+ @staticmethod
1271
+ def _reverse_dict (d ):
1272
+ return dict ((v , k ) for k , v in six .iteritems (d ))
1273
+
1247
1274
1248
1275
def _convert_binary (attr ):
1249
1276
if BINARY_SHORT in attr :
@@ -1252,3 +1279,23 @@ def _convert_binary(attr):
1252
1279
value = attr [BINARY_SET_SHORT ]
1253
1280
if value and len (value ):
1254
1281
attr [BINARY_SET_SHORT ] = set (b64decode (v .encode (DEFAULT_ENCODING )) for v in value )
1282
+
1283
+
1284
+ def _substitute_names (expression , placeholders ):
1285
+ """
1286
+ Replaces names in the given expression with placeholders.
1287
+ Stores the placeholders in the given dictionary.
1288
+ """
1289
+ path_segments = expression .split ('.' )
1290
+ for idx , segment in enumerate (path_segments ):
1291
+ match = PATH_SEGMENT_REGEX .match (segment )
1292
+ if not match :
1293
+ raise ValueError ('{0} is not a valid document path' .format (expression ))
1294
+ name , indexes = match .groups ()
1295
+ if name in placeholders :
1296
+ placeholder = placeholders [name ]
1297
+ else :
1298
+ placeholder = '#' + str (len (placeholders ))
1299
+ placeholders [name ] = placeholder
1300
+ path_segments [idx ] = placeholder + indexes
1301
+ return '.' .join (path_segments )
0 commit comments