comparison env/lib/python3.9/site-packages/boto/dynamodb/types.py @ 0:4f3585e2f14b draft default tip

"planemo upload commit 60cee0fc7c0cda8592644e1aad72851dec82c959"
author shellac
date Mon, 22 Mar 2021 18:12:50 +0000
parents
children
comparison
equal deleted inserted replaced
-1:000000000000 0:4f3585e2f14b
1 # Copyright (c) 2011 Mitch Garnaat http://garnaat.org/
2 # Copyright (c) 2011 Amazon.com, Inc. or its affiliates. All Rights Reserved
3 #
4 # Permission is hereby granted, free of charge, to any person obtaining a
5 # copy of this software and associated documentation files (the
6 # "Software"), to deal in the Software without restriction, including
7 # without limitation the rights to use, copy, modify, merge, publish, dis-
8 # tribute, sublicense, and/or sell copies of the Software, and to permit
9 # persons to whom the Software is furnished to do so, subject to the fol-
10 # lowing conditions:
11 #
12 # The above copyright notice and this permission notice shall be included
13 # in all copies or substantial portions of the Software.
14 #
15 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
16 # OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABIL-
17 # ITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
18 # SHALL THE AUTHOR BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
19 # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
21 # IN THE SOFTWARE.
22 #
23 """
24 Some utility functions to deal with mapping Amazon DynamoDB types to
25 Python types and vice-versa.
26 """
27 import base64
28 from decimal import (Decimal, DecimalException, Context,
29 Clamped, Overflow, Inexact, Underflow, Rounded)
30 from collections import Mapping
31 from boto.dynamodb.exceptions import DynamoDBNumberError
32 from boto.compat import filter, map, six, long_type
33
34
35 DYNAMODB_CONTEXT = Context(
36 Emin=-128, Emax=126, rounding=None, prec=38,
37 traps=[Clamped, Overflow, Inexact, Rounded, Underflow])
38
39
40 # python2.6 cannot convert floats directly to
41 # Decimals. This is taken from:
42 # http://docs.python.org/release/2.6.7/library/decimal.html#decimal-faq
43 def float_to_decimal(f):
44 n, d = f.as_integer_ratio()
45 numerator, denominator = Decimal(n), Decimal(d)
46 ctx = DYNAMODB_CONTEXT
47 result = ctx.divide(numerator, denominator)
48 while ctx.flags[Inexact]:
49 ctx.flags[Inexact] = False
50 ctx.prec *= 2
51 result = ctx.divide(numerator, denominator)
52 return result
53
54
55 def is_num(n, boolean_as_int=True):
56 if boolean_as_int:
57 types = (int, long_type, float, Decimal, bool)
58 else:
59 types = (int, long_type, float, Decimal)
60
61 return isinstance(n, types) or n in types
62
63
64 if six.PY2:
65 def is_str(n):
66 return (isinstance(n, basestring) or
67 isinstance(n, type) and issubclass(n, basestring))
68
69 def is_binary(n):
70 return isinstance(n, Binary)
71
72 else: # PY3
73 def is_str(n):
74 return (isinstance(n, str) or
75 isinstance(n, type) and issubclass(n, str))
76
77 def is_binary(n):
78 return isinstance(n, bytes) # Binary is subclass of bytes.
79
80
81 def serialize_num(val):
82 """Cast a number to a string and perform
83 validation to ensure no loss of precision.
84 """
85 if isinstance(val, bool):
86 return str(int(val))
87 return str(val)
88
89
90 def convert_num(s):
91 if '.' in s:
92 n = float(s)
93 else:
94 n = int(s)
95 return n
96
97
98 def convert_binary(n):
99 return Binary(base64.b64decode(n))
100
101
102 def get_dynamodb_type(val, use_boolean=True):
103 """
104 Take a scalar Python value and return a string representing
105 the corresponding Amazon DynamoDB type. If the value passed in is
106 not a supported type, raise a TypeError.
107 """
108 dynamodb_type = None
109 if val is None:
110 dynamodb_type = 'NULL'
111 elif is_num(val):
112 if isinstance(val, bool) and use_boolean:
113 dynamodb_type = 'BOOL'
114 else:
115 dynamodb_type = 'N'
116 elif is_str(val):
117 dynamodb_type = 'S'
118 elif isinstance(val, (set, frozenset)):
119 if False not in map(is_num, val):
120 dynamodb_type = 'NS'
121 elif False not in map(is_str, val):
122 dynamodb_type = 'SS'
123 elif False not in map(is_binary, val):
124 dynamodb_type = 'BS'
125 elif is_binary(val):
126 dynamodb_type = 'B'
127 elif isinstance(val, Mapping):
128 dynamodb_type = 'M'
129 elif isinstance(val, list):
130 dynamodb_type = 'L'
131 if dynamodb_type is None:
132 msg = 'Unsupported type "%s" for value "%s"' % (type(val), val)
133 raise TypeError(msg)
134 return dynamodb_type
135
136
137 def dynamize_value(val):
138 """
139 Take a scalar Python value and return a dict consisting
140 of the Amazon DynamoDB type specification and the value that
141 needs to be sent to Amazon DynamoDB. If the type of the value
142 is not supported, raise a TypeError
143 """
144 dynamodb_type = get_dynamodb_type(val)
145 if dynamodb_type == 'N':
146 val = {dynamodb_type: serialize_num(val)}
147 elif dynamodb_type == 'S':
148 val = {dynamodb_type: val}
149 elif dynamodb_type == 'NS':
150 val = {dynamodb_type: list(map(serialize_num, val))}
151 elif dynamodb_type == 'SS':
152 val = {dynamodb_type: [n for n in val]}
153 elif dynamodb_type == 'B':
154 if isinstance(val, bytes):
155 val = Binary(val)
156 val = {dynamodb_type: val.encode()}
157 elif dynamodb_type == 'BS':
158 val = {dynamodb_type: [n.encode() for n in val]}
159 return val
160
161
162 if six.PY2:
163 class Binary(object):
164 def __init__(self, value):
165 if not isinstance(value, (bytes, six.text_type)):
166 raise TypeError('Value must be a string of binary data!')
167 if not isinstance(value, bytes):
168 value = value.encode("utf-8")
169
170 self.value = value
171
172 def encode(self):
173 return base64.b64encode(self.value).decode('utf-8')
174
175 def __eq__(self, other):
176 if isinstance(other, Binary):
177 return self.value == other.value
178 else:
179 return self.value == other
180
181 def __ne__(self, other):
182 return not self.__eq__(other)
183
184 def __repr__(self):
185 return 'Binary(%r)' % self.value
186
187 def __str__(self):
188 return self.value
189
190 def __hash__(self):
191 return hash(self.value)
192 else:
193 class Binary(bytes):
194 def encode(self):
195 return base64.b64encode(self).decode('utf-8')
196
197 @property
198 def value(self):
199 # This matches the public API of the Python 2 version,
200 # but just returns itself since it is already a bytes
201 # instance.
202 return bytes(self)
203
204 def __repr__(self):
205 return 'Binary(%r)' % self.value
206
207
208 def item_object_hook(dct):
209 """
210 A custom object hook for use when decoding JSON item bodys.
211 This hook will transform Amazon DynamoDB JSON responses to something
212 that maps directly to native Python types.
213 """
214 if len(dct.keys()) > 1:
215 return dct
216 if 'S' in dct:
217 return dct['S']
218 if 'N' in dct:
219 return convert_num(dct['N'])
220 if 'SS' in dct:
221 return set(dct['SS'])
222 if 'NS' in dct:
223 return set(map(convert_num, dct['NS']))
224 if 'B' in dct:
225 return convert_binary(dct['B'])
226 if 'BS' in dct:
227 return set(map(convert_binary, dct['BS']))
228 return dct
229
230
231 class Dynamizer(object):
232 """Control serialization/deserialization of types.
233
234 This class controls the encoding of python types to the
235 format that is expected by the DynamoDB API, as well as
236 taking DynamoDB types and constructing the appropriate
237 python types.
238
239 If you want to customize this process, you can subclass
240 this class and override the encoding/decoding of
241 specific types. For example::
242
243 'foo' (Python type)
244 |
245 v
246 encode('foo')
247 |
248 v
249 _encode_s('foo')
250 |
251 v
252 {'S': 'foo'} (Encoding sent to/received from DynamoDB)
253 |
254 V
255 decode({'S': 'foo'})
256 |
257 v
258 _decode_s({'S': 'foo'})
259 |
260 v
261 'foo' (Python type)
262
263 """
264 def _get_dynamodb_type(self, attr):
265 return get_dynamodb_type(attr)
266
267 def encode(self, attr):
268 """
269 Encodes a python type to the format expected
270 by DynamoDB.
271
272 """
273 dynamodb_type = self._get_dynamodb_type(attr)
274 try:
275 encoder = getattr(self, '_encode_%s' % dynamodb_type.lower())
276 except AttributeError:
277 raise ValueError("Unable to encode dynamodb type: %s" %
278 dynamodb_type)
279 return {dynamodb_type: encoder(attr)}
280
281 def _encode_n(self, attr):
282 try:
283 if isinstance(attr, float) and not hasattr(Decimal, 'from_float'):
284 # python2.6 does not support creating Decimals directly
285 # from floats so we have to do this ourself.
286 n = str(float_to_decimal(attr))
287 else:
288 n = str(DYNAMODB_CONTEXT.create_decimal(attr))
289 if list(filter(lambda x: x in n, ('Infinity', 'NaN'))):
290 raise TypeError('Infinity and NaN not supported')
291 return n
292 except (TypeError, DecimalException) as e:
293 msg = '{0} numeric for `{1}`\n{2}'.format(
294 e.__class__.__name__, attr, str(e) or '')
295 raise DynamoDBNumberError(msg)
296
297 def _encode_s(self, attr):
298 if isinstance(attr, bytes):
299 attr = attr.decode('utf-8')
300 elif not isinstance(attr, six.text_type):
301 attr = str(attr)
302 return attr
303
304 def _encode_ns(self, attr):
305 return list(map(self._encode_n, attr))
306
307 def _encode_ss(self, attr):
308 return [self._encode_s(n) for n in attr]
309
310 def _encode_b(self, attr):
311 if isinstance(attr, bytes):
312 attr = Binary(attr)
313 return attr.encode()
314
315 def _encode_bs(self, attr):
316 return [self._encode_b(n) for n in attr]
317
318 def _encode_null(self, attr):
319 return True
320
321 def _encode_bool(self, attr):
322 return attr
323
324 def _encode_m(self, attr):
325 return dict([(k, self.encode(v)) for k, v in attr.items()])
326
327 def _encode_l(self, attr):
328 return [self.encode(i) for i in attr]
329
330 def decode(self, attr):
331 """
332 Takes the format returned by DynamoDB and constructs
333 the appropriate python type.
334
335 """
336 if len(attr) > 1 or not attr or is_str(attr):
337 return attr
338 dynamodb_type = list(attr.keys())[0]
339 if dynamodb_type.lower() == dynamodb_type:
340 # It's not an actual type, just a single character attr that
341 # overlaps with the DDB types. Return it.
342 return attr
343 try:
344 decoder = getattr(self, '_decode_%s' % dynamodb_type.lower())
345 except AttributeError:
346 return attr
347 return decoder(attr[dynamodb_type])
348
349 def _decode_n(self, attr):
350 return DYNAMODB_CONTEXT.create_decimal(attr)
351
352 def _decode_s(self, attr):
353 return attr
354
355 def _decode_ns(self, attr):
356 return set(map(self._decode_n, attr))
357
358 def _decode_ss(self, attr):
359 return set(map(self._decode_s, attr))
360
361 def _decode_b(self, attr):
362 return convert_binary(attr)
363
364 def _decode_bs(self, attr):
365 return set(map(self._decode_b, attr))
366
367 def _decode_null(self, attr):
368 return None
369
370 def _decode_bool(self, attr):
371 return attr
372
373 def _decode_m(self, attr):
374 return dict([(k, self.decode(v)) for k, v in attr.items()])
375
376 def _decode_l(self, attr):
377 return [self.decode(i) for i in attr]
378
379
380 class NonBooleanDynamizer(Dynamizer):
381 """Casting boolean type to numeric types.
382
383 This class is provided for backward compatibility.
384 """
385 def _get_dynamodb_type(self, attr):
386 return get_dynamodb_type(attr, use_boolean=False)
387
388
389 class LossyFloatDynamizer(NonBooleanDynamizer):
390 """Use float/int instead of Decimal for numeric types.
391
392 This class is provided for backwards compatibility. Instead of
393 using Decimals for the 'N', 'NS' types it uses ints/floats.
394
395 This class is deprecated and its usage is not encouraged,
396 as doing so may result in loss of precision. Use the
397 `Dynamizer` class instead.
398
399 """
400 def _encode_n(self, attr):
401 return serialize_num(attr)
402
403 def _encode_ns(self, attr):
404 return [str(i) for i in attr]
405
406 def _decode_n(self, attr):
407 return convert_num(attr)
408
409 def _decode_ns(self, attr):
410 return set(map(self._decode_n, attr))