Skip to content

Commit 3f0b9f1

Browse files
committed
2.2.0 -- minor release to prepare for upgrading to 3.0.1
1 parent 67e88ca commit 3f0b9f1

File tree

4 files changed

+140
-2
lines changed

4 files changed

+140
-2
lines changed

pynamodb/__init__.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,4 @@
77
"""
88
__author__ = 'Jharrod LaFon'
99
__license__ = 'MIT'
10-
__version__ = '2.1.6'
10+
__version__ = '2.2.0'

pynamodb/attributes.py

+12
Original file line numberDiff line numberDiff line change
@@ -530,6 +530,18 @@ def _get_deserialize_class(cls, key, value):
530530
return cls._get_attributes().get(key)
531531
return _get_class_for_deserialize(value)
532532

533+
@classmethod
534+
def _has_unicode_set_attribute(cls):
535+
if cls.is_raw():
536+
return False
537+
538+
for attr_value in cls._get_attributes().values():
539+
if isinstance(attr_value, UnicodeSetAttribute):
540+
return True
541+
if isinstance(attr_value, MapAttribute):
542+
return attr_value._has_unicode_set_attribute()
543+
544+
return False
533545

534546
def _get_value_for_deserialize(value):
535547
key = next(iter(value.keys()))

pynamodb/models.py

+90-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
from six import add_metaclass
1212
from pynamodb.exceptions import DoesNotExist, TableDoesNotExist, TableError
1313
from pynamodb.throttle import NoThrottle
14-
from pynamodb.attributes import Attribute, AttributeContainer, MapAttribute, ListAttribute
14+
from pynamodb.attributes import Attribute, AttributeContainer, MapAttribute, ListAttribute, UnicodeSetAttribute
1515
from pynamodb.connection.base import MetaTable
1616
from pynamodb.connection.table import TableConnection
1717
from pynamodb.connection.util import pythonic
@@ -233,6 +233,95 @@ def __init__(self, hash_key=None, range_key=None, **attrs):
233233
attrs[self._dynamo_to_python_attr(range_keyname)] = range_key
234234
self._set_attributes(**attrs)
235235

236+
@classmethod
237+
def fix_unicode_set_attributes(cls,
238+
get_save_kwargs,
239+
read_capacity_to_consume_per_second=10,
240+
max_sleep_between_retry=10,
241+
max_consecutive_exceptions=30):
242+
"""
243+
This function performs a rate limited scan of the table and re-serializes any UnicodeSetAttributes.
244+
245+
See https://github.com/pynamodb/PynamoDB/issues/377 for why this is necessary.
246+
247+
:param get_save_kwargs: A callback function that is passed a model and should return the kwargs
248+
used when conditionally saving the item
249+
:param read_capacity_to_consume_per_second: Amount of read capacity to consume
250+
every second
251+
:param max_sleep_between_retry: Max value for sleep in seconds in between scans during
252+
throttling/rate limit scenarios
253+
:param max_consecutive_exceptions: Max number of consecutive provision throughput exceeded
254+
exceptions for scan to exit
255+
"""
256+
257+
if not cls._has_unicode_set_attribute():
258+
return
259+
260+
items = cls.rate_limited_scan(
261+
read_capacity_to_consume_per_second=read_capacity_to_consume_per_second,
262+
max_sleep_between_retry=max_sleep_between_retry,
263+
max_consecutive_exceptions=max_consecutive_exceptions
264+
)
265+
for item in items:
266+
save_kwargs = get_save_kwargs(item)
267+
item.save(**save_kwargs)
268+
269+
@classmethod
270+
def _has_unicode_set_attribute(cls):
271+
for attr_value in cls._get_attributes().values():
272+
if isinstance(attr_value, UnicodeSetAttribute):
273+
return True
274+
if isinstance(attr_value, MapAttribute):
275+
return attr_value._has_unicode_set_attribute()
276+
return False
277+
278+
@classmethod
279+
def needs_unicode_set_fix(cls,
280+
read_capacity_to_consume_per_second=10,
281+
max_sleep_between_retry=10,
282+
max_consecutive_exceptions=30):
283+
284+
if not cls._has_unicode_set_attribute():
285+
return False
286+
287+
scan_result = cls._get_connection().rate_limited_scan(
288+
read_capacity_to_consume_per_second=read_capacity_to_consume_per_second,
289+
max_sleep_between_retry=max_sleep_between_retry,
290+
max_consecutive_exceptions=max_consecutive_exceptions,
291+
)
292+
293+
ret_val = False
294+
for item in scan_result:
295+
ret_val |= cls._has_json_unicode_set_value(item)
296+
return ret_val
297+
298+
@classmethod
299+
def _has_json_unicode_set_value(cls, data):
300+
ret_val = False
301+
for name, attr in data.items():
302+
# attr should be a map of attribute type to value
303+
(attr_type, attr_value), = attr.items()
304+
if attr_type == 'M':
305+
ret_val |= cls._has_json_unicode_set_value(attr_value)
306+
elif attr_type == 'L':
307+
for value in attr_value:
308+
(at, av), = value.items()
309+
ret_val |= cls._attr_has_json_unicode_set_value(at, av)
310+
else:
311+
ret_val |= cls._attr_has_json_unicode_set_value(attr_type, attr_value)
312+
return ret_val
313+
314+
@classmethod
315+
def _attr_has_json_unicode_set_value(cls, attr_type, value):
316+
if attr_type != 'SS':
317+
return False
318+
for val in value:
319+
try:
320+
result = json.loads(val)
321+
except ValueError:
322+
return False
323+
return True
324+
236325
@classmethod
237326
def has_map_or_list_attributes(cls):
238327
for attr_value in cls._get_attributes().values():

pynamodb/tests/test_model.py

+37
Original file line numberDiff line numberDiff line change
@@ -3619,3 +3619,40 @@ def test_subclassed_map_attribute_with_map_attribute_member_with_initialized_ins
36193619
self.assertEquals(actual.left.left.value, left_instance.left.value)
36203620
self.assertEquals(actual.right.right.left.value, right_instance.right.left.value)
36213621
self.assertEquals(actual.right.right.value, right_instance.right.value)
3622+
3623+
3624+
class JSONUnicodeSetTestCase(TestCase):
3625+
3626+
def test_needs_unidode_set_fix_false(self):
3627+
test_item = {
3628+
'string_set_attr': {
3629+
'SS': ['a', 'b']
3630+
},
3631+
'map_attr': {'M': {
3632+
'foo': {'S': 'bar'},
3633+
'num': {'N': '1'},
3634+
'bool_type': {'BOOL': True},
3635+
'other_b_type': {'BOOL': False},
3636+
'floaty': {'N': '1.2'},
3637+
'listy': {'L': [{'N': '1'}, {'N': '2'}, {'N': '12345678909876543211234234324234'}]},
3638+
'mapy': {'M': {'baz': {'S': 'bongo'}}}
3639+
}}
3640+
}
3641+
assert Model._has_json_unicode_set_value(test_item) == False
3642+
3643+
def test_needs_unidode_set_fix_true(self):
3644+
test_item = {
3645+
'string_set_attr': {
3646+
'SS': ['a', 'b']
3647+
},
3648+
'map_attr': {'M': {
3649+
'foo': {'S': 'bar'},
3650+
'num': {'N': '1'},
3651+
'bool_type': {'BOOL': True},
3652+
'other_b_type': {'BOOL': False},
3653+
'floaty': {'N': '1.2'},
3654+
'listy': {'L': [{'N': '1'}, {'N': '2'}, {'SS': ['"a"', '"b"']}]},
3655+
'mapy': {'M': {'baz': {'S': 'bongo'}}}
3656+
}}
3657+
}
3658+
assert Model._has_json_unicode_set_value(test_item) == True

0 commit comments

Comments
 (0)