Mercurial > repos > shellac > sam_consensus_v3
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)) |