PageRenderTime 191ms CodeModel.GetById 5ms app.highlight 165ms RepoModel.GetById 1ms app.codeStats 1ms

/y/google-cloud-sdk/platform/gsutil/third_party/boto/tests/unit/dynamodb2/test_table.py

https://bitbucket.org/kartik894/assignment6
Python | 3068 lines | 2821 code | 171 blank | 76 comment | 28 complexity | dd4ffca3cd58e4f194f6e82094e89047 MD5 | raw file
   1from tests.compat import mock, unittest
   2from boto.dynamodb2 import exceptions
   3from boto.dynamodb2.fields import (HashKey, RangeKey,
   4                                   AllIndex, KeysOnlyIndex, IncludeIndex,
   5                                   GlobalAllIndex, GlobalKeysOnlyIndex,
   6                                   GlobalIncludeIndex)
   7from boto.dynamodb2.items import Item
   8from boto.dynamodb2.layer1 import DynamoDBConnection
   9from boto.dynamodb2.results import ResultSet, BatchGetResultSet
  10from boto.dynamodb2.table import Table
  11from boto.dynamodb2.types import (STRING, NUMBER, BINARY,
  12                                  FILTER_OPERATORS, QUERY_OPERATORS)
  13from boto.exception import JSONResponseError
  14from boto.compat import six, long_type
  15
  16
  17FakeDynamoDBConnection = mock.create_autospec(DynamoDBConnection)
  18
  19
  20class SchemaFieldsTestCase(unittest.TestCase):
  21    def test_hash_key(self):
  22        hash_key = HashKey('hello')
  23        self.assertEqual(hash_key.name, 'hello')
  24        self.assertEqual(hash_key.data_type, STRING)
  25        self.assertEqual(hash_key.attr_type, 'HASH')
  26
  27        self.assertEqual(hash_key.definition(), {
  28            'AttributeName': 'hello',
  29            'AttributeType': 'S'
  30        })
  31        self.assertEqual(hash_key.schema(), {
  32            'AttributeName': 'hello',
  33            'KeyType': 'HASH'
  34        })
  35
  36    def test_range_key(self):
  37        range_key = RangeKey('hello')
  38        self.assertEqual(range_key.name, 'hello')
  39        self.assertEqual(range_key.data_type, STRING)
  40        self.assertEqual(range_key.attr_type, 'RANGE')
  41
  42        self.assertEqual(range_key.definition(), {
  43            'AttributeName': 'hello',
  44            'AttributeType': 'S'
  45        })
  46        self.assertEqual(range_key.schema(), {
  47            'AttributeName': 'hello',
  48            'KeyType': 'RANGE'
  49        })
  50
  51    def test_alternate_type(self):
  52        alt_key = HashKey('alt', data_type=NUMBER)
  53        self.assertEqual(alt_key.name, 'alt')
  54        self.assertEqual(alt_key.data_type, NUMBER)
  55        self.assertEqual(alt_key.attr_type, 'HASH')
  56
  57        self.assertEqual(alt_key.definition(), {
  58            'AttributeName': 'alt',
  59            'AttributeType': 'N'
  60        })
  61        self.assertEqual(alt_key.schema(), {
  62            'AttributeName': 'alt',
  63            'KeyType': 'HASH'
  64        })
  65
  66
  67class IndexFieldTestCase(unittest.TestCase):
  68    def test_all_index(self):
  69        all_index = AllIndex('AllKeys', parts=[
  70            HashKey('username'),
  71            RangeKey('date_joined')
  72        ])
  73        self.assertEqual(all_index.name, 'AllKeys')
  74        self.assertEqual([part.attr_type for part in all_index.parts], [
  75            'HASH',
  76            'RANGE'
  77        ])
  78        self.assertEqual(all_index.projection_type, 'ALL')
  79
  80        self.assertEqual(all_index.definition(), [
  81            {'AttributeName': 'username', 'AttributeType': 'S'},
  82            {'AttributeName': 'date_joined', 'AttributeType': 'S'}
  83        ])
  84        self.assertEqual(all_index.schema(), {
  85            'IndexName': 'AllKeys',
  86            'KeySchema': [
  87                {
  88                    'AttributeName': 'username',
  89                    'KeyType': 'HASH'
  90                },
  91                {
  92                    'AttributeName': 'date_joined',
  93                    'KeyType': 'RANGE'
  94                }
  95            ],
  96            'Projection': {
  97                'ProjectionType': 'ALL'
  98            }
  99        })
 100
 101    def test_keys_only_index(self):
 102        keys_only = KeysOnlyIndex('KeysOnly', parts=[
 103            HashKey('username'),
 104            RangeKey('date_joined')
 105        ])
 106        self.assertEqual(keys_only.name, 'KeysOnly')
 107        self.assertEqual([part.attr_type for part in keys_only.parts], [
 108            'HASH',
 109            'RANGE'
 110        ])
 111        self.assertEqual(keys_only.projection_type, 'KEYS_ONLY')
 112
 113        self.assertEqual(keys_only.definition(), [
 114            {'AttributeName': 'username', 'AttributeType': 'S'},
 115            {'AttributeName': 'date_joined', 'AttributeType': 'S'}
 116        ])
 117        self.assertEqual(keys_only.schema(), {
 118            'IndexName': 'KeysOnly',
 119            'KeySchema': [
 120                {
 121                    'AttributeName': 'username',
 122                    'KeyType': 'HASH'
 123                },
 124                {
 125                    'AttributeName': 'date_joined',
 126                    'KeyType': 'RANGE'
 127                }
 128            ],
 129            'Projection': {
 130                'ProjectionType': 'KEYS_ONLY'
 131            }
 132        })
 133
 134    def test_include_index(self):
 135        include_index = IncludeIndex('IncludeKeys', parts=[
 136            HashKey('username'),
 137            RangeKey('date_joined')
 138        ], includes=[
 139            'gender',
 140            'friend_count'
 141        ])
 142        self.assertEqual(include_index.name, 'IncludeKeys')
 143        self.assertEqual([part.attr_type for part in include_index.parts], [
 144            'HASH',
 145            'RANGE'
 146        ])
 147        self.assertEqual(include_index.projection_type, 'INCLUDE')
 148
 149        self.assertEqual(include_index.definition(), [
 150            {'AttributeName': 'username', 'AttributeType': 'S'},
 151            {'AttributeName': 'date_joined', 'AttributeType': 'S'}
 152        ])
 153        self.assertEqual(include_index.schema(), {
 154            'IndexName': 'IncludeKeys',
 155            'KeySchema': [
 156                {
 157                    'AttributeName': 'username',
 158                    'KeyType': 'HASH'
 159                },
 160                {
 161                    'AttributeName': 'date_joined',
 162                    'KeyType': 'RANGE'
 163                }
 164            ],
 165            'Projection': {
 166                'ProjectionType': 'INCLUDE',
 167                'NonKeyAttributes': [
 168                    'gender',
 169                    'friend_count',
 170                ]
 171            }
 172        })
 173
 174    def test_global_all_index(self):
 175        all_index = GlobalAllIndex('AllKeys', parts=[
 176            HashKey('username'),
 177            RangeKey('date_joined')
 178        ],
 179        throughput={
 180            'read': 6,
 181            'write': 2,
 182        })
 183        self.assertEqual(all_index.name, 'AllKeys')
 184        self.assertEqual([part.attr_type for part in all_index.parts], [
 185            'HASH',
 186            'RANGE'
 187        ])
 188        self.assertEqual(all_index.projection_type, 'ALL')
 189
 190        self.assertEqual(all_index.definition(), [
 191            {'AttributeName': 'username', 'AttributeType': 'S'},
 192            {'AttributeName': 'date_joined', 'AttributeType': 'S'}
 193        ])
 194        self.assertEqual(all_index.schema(), {
 195            'IndexName': 'AllKeys',
 196            'KeySchema': [
 197                {
 198                    'AttributeName': 'username',
 199                    'KeyType': 'HASH'
 200                },
 201                {
 202                    'AttributeName': 'date_joined',
 203                    'KeyType': 'RANGE'
 204                }
 205            ],
 206            'Projection': {
 207                'ProjectionType': 'ALL'
 208            },
 209            'ProvisionedThroughput': {
 210                'ReadCapacityUnits': 6,
 211                'WriteCapacityUnits': 2
 212            }
 213        })
 214
 215    def test_global_keys_only_index(self):
 216        keys_only = GlobalKeysOnlyIndex('KeysOnly', parts=[
 217            HashKey('username'),
 218            RangeKey('date_joined')
 219        ],
 220        throughput={
 221            'read': 3,
 222            'write': 4,
 223        })
 224        self.assertEqual(keys_only.name, 'KeysOnly')
 225        self.assertEqual([part.attr_type for part in keys_only.parts], [
 226            'HASH',
 227            'RANGE'
 228        ])
 229        self.assertEqual(keys_only.projection_type, 'KEYS_ONLY')
 230
 231        self.assertEqual(keys_only.definition(), [
 232            {'AttributeName': 'username', 'AttributeType': 'S'},
 233            {'AttributeName': 'date_joined', 'AttributeType': 'S'}
 234        ])
 235        self.assertEqual(keys_only.schema(), {
 236            'IndexName': 'KeysOnly',
 237            'KeySchema': [
 238                {
 239                    'AttributeName': 'username',
 240                    'KeyType': 'HASH'
 241                },
 242                {
 243                    'AttributeName': 'date_joined',
 244                    'KeyType': 'RANGE'
 245                }
 246            ],
 247            'Projection': {
 248                'ProjectionType': 'KEYS_ONLY'
 249            },
 250            'ProvisionedThroughput': {
 251                'ReadCapacityUnits': 3,
 252                'WriteCapacityUnits': 4
 253            }
 254        })
 255
 256    def test_global_include_index(self):
 257        # Lean on the default throughput
 258        include_index = GlobalIncludeIndex('IncludeKeys', parts=[
 259            HashKey('username'),
 260            RangeKey('date_joined')
 261        ], includes=[
 262            'gender',
 263            'friend_count'
 264        ])
 265        self.assertEqual(include_index.name, 'IncludeKeys')
 266        self.assertEqual([part.attr_type for part in include_index.parts], [
 267            'HASH',
 268            'RANGE'
 269        ])
 270        self.assertEqual(include_index.projection_type, 'INCLUDE')
 271
 272        self.assertEqual(include_index.definition(), [
 273            {'AttributeName': 'username', 'AttributeType': 'S'},
 274            {'AttributeName': 'date_joined', 'AttributeType': 'S'}
 275        ])
 276        self.assertEqual(include_index.schema(), {
 277            'IndexName': 'IncludeKeys',
 278            'KeySchema': [
 279                {
 280                    'AttributeName': 'username',
 281                    'KeyType': 'HASH'
 282                },
 283                {
 284                    'AttributeName': 'date_joined',
 285                    'KeyType': 'RANGE'
 286                }
 287            ],
 288            'Projection': {
 289                'ProjectionType': 'INCLUDE',
 290                'NonKeyAttributes': [
 291                    'gender',
 292                    'friend_count',
 293                ]
 294            },
 295            'ProvisionedThroughput': {
 296                'ReadCapacityUnits': 5,
 297                'WriteCapacityUnits': 5
 298            }
 299        })
 300
 301    def test_global_include_index_throughput(self):
 302        include_index = GlobalIncludeIndex('IncludeKeys', parts=[
 303            HashKey('username'),
 304            RangeKey('date_joined')
 305        ], includes=[
 306            'gender',
 307            'friend_count'
 308        ], throughput={
 309            'read': 10,
 310            'write': 8
 311        })
 312
 313        self.assertEqual(include_index.schema(), {
 314            'IndexName': 'IncludeKeys',
 315            'KeySchema': [
 316                {
 317                    'AttributeName': 'username',
 318                    'KeyType': 'HASH'
 319                },
 320                {
 321                    'AttributeName': 'date_joined',
 322                    'KeyType': 'RANGE'
 323                }
 324            ],
 325            'Projection': {
 326                'ProjectionType': 'INCLUDE',
 327                'NonKeyAttributes': [
 328                    'gender',
 329                    'friend_count',
 330                ]
 331            },
 332            'ProvisionedThroughput': {
 333                'ReadCapacityUnits': 10,
 334                'WriteCapacityUnits': 8
 335            }
 336        })
 337
 338
 339class ItemTestCase(unittest.TestCase):
 340    if six.PY2:
 341        assertCountEqual = unittest.TestCase.assertItemsEqual
 342
 343    def setUp(self):
 344        super(ItemTestCase, self).setUp()
 345        self.table = Table('whatever', connection=FakeDynamoDBConnection())
 346        self.johndoe = self.create_item({
 347            'username': 'johndoe',
 348            'first_name': 'John',
 349            'date_joined': 12345,
 350        })
 351
 352    def create_item(self, data):
 353        return Item(self.table, data=data)
 354
 355    def test_initialization(self):
 356        empty_item = Item(self.table)
 357        self.assertEqual(empty_item.table, self.table)
 358        self.assertEqual(empty_item._data, {})
 359
 360        full_item = Item(self.table, data={
 361            'username': 'johndoe',
 362            'date_joined': 12345,
 363        })
 364        self.assertEqual(full_item.table, self.table)
 365        self.assertEqual(full_item._data, {
 366            'username': 'johndoe',
 367            'date_joined': 12345,
 368        })
 369
 370    # The next couple methods make use of ``sorted(...)`` so we get consistent
 371    # ordering everywhere & no erroneous failures.
 372
 373    def test_keys(self):
 374        self.assertCountEqual(self.johndoe.keys(), [
 375            'date_joined',
 376            'first_name',
 377            'username',
 378        ])
 379
 380    def test_values(self):
 381        self.assertCountEqual(self.johndoe.values(),
 382                              [12345, 'John', 'johndoe'])
 383
 384    def test_contains(self):
 385        self.assertIn('username', self.johndoe)
 386        self.assertIn('first_name', self.johndoe)
 387        self.assertIn('date_joined', self.johndoe)
 388        self.assertNotIn('whatever', self.johndoe)
 389
 390    def test_iter(self):
 391        self.assertCountEqual(self.johndoe,
 392                              ['johndoe', 'John', 12345])
 393
 394    def test_get(self):
 395        self.assertEqual(self.johndoe.get('username'), 'johndoe')
 396        self.assertEqual(self.johndoe.get('first_name'), 'John')
 397        self.assertEqual(self.johndoe.get('date_joined'), 12345)
 398
 399        # Test a missing key. No default yields ``None``.
 400        self.assertEqual(self.johndoe.get('last_name'), None)
 401        # This time with a default.
 402        self.assertEqual(self.johndoe.get('last_name', True), True)
 403
 404    def test_items(self):
 405        self.assertCountEqual(
 406            self.johndoe.items(),
 407            [
 408                ('date_joined', 12345),
 409                ('first_name', 'John'),
 410                ('username', 'johndoe'),
 411            ])
 412
 413    def test_attribute_access(self):
 414        self.assertEqual(self.johndoe['username'], 'johndoe')
 415        self.assertEqual(self.johndoe['first_name'], 'John')
 416        self.assertEqual(self.johndoe['date_joined'], 12345)
 417
 418        # Test a missing key.
 419        self.assertEqual(self.johndoe['last_name'], None)
 420
 421        # Set a key.
 422        self.johndoe['last_name'] = 'Doe'
 423        # Test accessing the new key.
 424        self.assertEqual(self.johndoe['last_name'], 'Doe')
 425
 426        # Delete a key.
 427        del self.johndoe['last_name']
 428        # Test the now-missing-again key.
 429        self.assertEqual(self.johndoe['last_name'], None)
 430
 431    def test_needs_save(self):
 432        self.johndoe.mark_clean()
 433        self.assertFalse(self.johndoe.needs_save())
 434        self.johndoe['last_name'] = 'Doe'
 435        self.assertTrue(self.johndoe.needs_save())
 436
 437    def test_needs_save_set_changed(self):
 438        # First, ensure we're clean.
 439        self.johndoe.mark_clean()
 440        self.assertFalse(self.johndoe.needs_save())
 441        # Add a friends collection.
 442        self.johndoe['friends'] = set(['jane', 'alice'])
 443        self.assertTrue(self.johndoe.needs_save())
 444        # Now mark it clean, then change the collection.
 445        # This does NOT call ``__setitem__``, so the item used to be
 446        # incorrectly appearing to be clean, when it had in fact been changed.
 447        self.johndoe.mark_clean()
 448        self.assertFalse(self.johndoe.needs_save())
 449        self.johndoe['friends'].add('bob')
 450        self.assertTrue(self.johndoe.needs_save())
 451
 452    def test_mark_clean(self):
 453        self.johndoe['last_name'] = 'Doe'
 454        self.assertTrue(self.johndoe.needs_save())
 455        self.johndoe.mark_clean()
 456        self.assertFalse(self.johndoe.needs_save())
 457
 458    def test_load(self):
 459        empty_item = Item(self.table)
 460        empty_item.load({
 461            'Item': {
 462                'username': {'S': 'johndoe'},
 463                'first_name': {'S': 'John'},
 464                'last_name': {'S': 'Doe'},
 465                'date_joined': {'N': '1366056668'},
 466                'friend_count': {'N': '3'},
 467                'friends': {'SS': ['alice', 'bob', 'jane']},
 468            }
 469        })
 470        self.assertEqual(empty_item['username'], 'johndoe')
 471        self.assertEqual(empty_item['date_joined'], 1366056668)
 472        self.assertEqual(sorted(empty_item['friends']), sorted([
 473            'alice',
 474            'bob',
 475            'jane'
 476        ]))
 477
 478    def test_get_keys(self):
 479        # Setup the data.
 480        self.table.schema = [
 481            HashKey('username'),
 482            RangeKey('date_joined'),
 483        ]
 484        self.assertEqual(self.johndoe.get_keys(), {
 485            'username': 'johndoe',
 486            'date_joined': 12345,
 487        })
 488
 489    def test_get_raw_keys(self):
 490        # Setup the data.
 491        self.table.schema = [
 492            HashKey('username'),
 493            RangeKey('date_joined'),
 494        ]
 495        self.assertEqual(self.johndoe.get_raw_keys(), {
 496            'username': {'S': 'johndoe'},
 497            'date_joined': {'N': '12345'},
 498        })
 499
 500    def test_build_expects(self):
 501        # Pristine.
 502        self.assertEqual(self.johndoe.build_expects(), {
 503            'first_name': {
 504                'Exists': False,
 505            },
 506            'username': {
 507                'Exists': False,
 508            },
 509            'date_joined': {
 510                'Exists': False,
 511            },
 512        })
 513
 514        # Without modifications.
 515        self.johndoe.mark_clean()
 516        self.assertEqual(self.johndoe.build_expects(), {
 517            'first_name': {
 518                'Exists': True,
 519                'Value': {
 520                    'S': 'John',
 521                },
 522            },
 523            'username': {
 524                'Exists': True,
 525                'Value': {
 526                    'S': 'johndoe',
 527                },
 528            },
 529            'date_joined': {
 530                'Exists': True,
 531                'Value': {
 532                    'N': '12345',
 533                },
 534            },
 535        })
 536
 537        # Change some data.
 538        self.johndoe['first_name'] = 'Johann'
 539        # Add some data.
 540        self.johndoe['last_name'] = 'Doe'
 541        # Delete some data.
 542        del self.johndoe['date_joined']
 543
 544        # All fields (default).
 545        self.assertEqual(self.johndoe.build_expects(), {
 546            'first_name': {
 547                'Exists': True,
 548                'Value': {
 549                    'S': 'John',
 550                },
 551            },
 552            'last_name': {
 553                'Exists': False,
 554            },
 555            'username': {
 556                'Exists': True,
 557                'Value': {
 558                    'S': 'johndoe',
 559                },
 560            },
 561            'date_joined': {
 562                'Exists': True,
 563                'Value': {
 564                    'N': '12345',
 565                },
 566            },
 567        })
 568
 569        # Only a subset of the fields.
 570        self.assertEqual(self.johndoe.build_expects(fields=[
 571            'first_name',
 572            'last_name',
 573            'date_joined',
 574        ]), {
 575            'first_name': {
 576                'Exists': True,
 577                'Value': {
 578                    'S': 'John',
 579                },
 580            },
 581            'last_name': {
 582                'Exists': False,
 583            },
 584            'date_joined': {
 585                'Exists': True,
 586                'Value': {
 587                    'N': '12345',
 588                },
 589            },
 590        })
 591
 592    def test_prepare_full(self):
 593        self.assertEqual(self.johndoe.prepare_full(), {
 594            'username': {'S': 'johndoe'},
 595            'first_name': {'S': 'John'},
 596            'date_joined': {'N': '12345'}
 597        })
 598
 599        self.johndoe['friends'] = set(['jane', 'alice'])
 600        data = self.johndoe.prepare_full()
 601        self.assertEqual(data['username'], {'S': 'johndoe'})
 602        self.assertEqual(data['first_name'], {'S': 'John'})
 603        self.assertEqual(data['date_joined'], {'N': '12345'})
 604        self.assertCountEqual(data['friends']['SS'],
 605                              ['jane', 'alice'])
 606
 607    def test_prepare_full_empty_set(self):
 608        self.johndoe['friends'] = set()
 609        self.assertEqual(self.johndoe.prepare_full(), {
 610            'username': {'S': 'johndoe'},
 611            'first_name': {'S': 'John'},
 612            'date_joined': {'N': '12345'}
 613        })
 614
 615    def test_prepare_partial(self):
 616        self.johndoe.mark_clean()
 617        # Change some data.
 618        self.johndoe['first_name'] = 'Johann'
 619        # Add some data.
 620        self.johndoe['last_name'] = 'Doe'
 621        # Delete some data.
 622        del self.johndoe['date_joined']
 623
 624        final_data, fields = self.johndoe.prepare_partial()
 625        self.assertEqual(final_data, {
 626            'date_joined': {
 627                'Action': 'DELETE',
 628            },
 629            'first_name': {
 630                'Action': 'PUT',
 631                'Value': {'S': 'Johann'},
 632            },
 633            'last_name': {
 634                'Action': 'PUT',
 635                'Value': {'S': 'Doe'},
 636            },
 637        })
 638        self.assertEqual(fields, set([
 639            'first_name',
 640            'last_name',
 641            'date_joined'
 642        ]))
 643
 644    def test_prepare_partial_empty_set(self):
 645        self.johndoe.mark_clean()
 646        # Change some data.
 647        self.johndoe['first_name'] = 'Johann'
 648        # Add some data.
 649        self.johndoe['last_name'] = 'Doe'
 650        # Delete some data.
 651        del self.johndoe['date_joined']
 652        # Put an empty set on the ``Item``.
 653        self.johndoe['friends'] = set()
 654
 655        final_data, fields = self.johndoe.prepare_partial()
 656        self.assertEqual(final_data, {
 657            'date_joined': {
 658                'Action': 'DELETE',
 659            },
 660            'first_name': {
 661                'Action': 'PUT',
 662                'Value': {'S': 'Johann'},
 663            },
 664            'last_name': {
 665                'Action': 'PUT',
 666                'Value': {'S': 'Doe'},
 667            },
 668        })
 669        self.assertEqual(fields, set([
 670            'first_name',
 671            'last_name',
 672            'date_joined'
 673        ]))
 674
 675    def test_save_no_changes(self):
 676        # Unchanged, no save.
 677        with mock.patch.object(self.table, '_put_item', return_value=True) \
 678                as mock_put_item:
 679            # Pretend we loaded it via ``get_item``...
 680            self.johndoe.mark_clean()
 681            self.assertFalse(self.johndoe.save())
 682
 683        self.assertFalse(mock_put_item.called)
 684
 685    def test_save_with_changes(self):
 686        # With changed data.
 687        with mock.patch.object(self.table, '_put_item', return_value=True) \
 688                as mock_put_item:
 689            self.johndoe.mark_clean()
 690            self.johndoe['first_name'] = 'J'
 691            self.johndoe['new_attr'] = 'never_seen_before'
 692            self.assertTrue(self.johndoe.save())
 693            self.assertFalse(self.johndoe.needs_save())
 694
 695        self.assertTrue(mock_put_item.called)
 696        mock_put_item.assert_called_once_with({
 697            'username': {'S': 'johndoe'},
 698            'first_name': {'S': 'J'},
 699            'new_attr': {'S': 'never_seen_before'},
 700            'date_joined': {'N': '12345'}
 701        }, expects={
 702            'username': {
 703                'Value': {
 704                    'S': 'johndoe',
 705                },
 706                'Exists': True,
 707            },
 708            'first_name': {
 709                'Value': {
 710                    'S': 'John',
 711                },
 712                'Exists': True,
 713            },
 714            'new_attr': {
 715                'Exists': False,
 716            },
 717            'date_joined': {
 718                'Value': {
 719                    'N': '12345',
 720                },
 721                'Exists': True,
 722            },
 723        })
 724
 725    def test_save_with_changes_overwrite(self):
 726        # With changed data.
 727        with mock.patch.object(self.table, '_put_item', return_value=True) \
 728                as mock_put_item:
 729            self.johndoe['first_name'] = 'J'
 730            self.johndoe['new_attr'] = 'never_seen_before'
 731            # OVERWRITE ALL THE THINGS
 732            self.assertTrue(self.johndoe.save(overwrite=True))
 733            self.assertFalse(self.johndoe.needs_save())
 734
 735        self.assertTrue(mock_put_item.called)
 736        mock_put_item.assert_called_once_with({
 737            'username': {'S': 'johndoe'},
 738            'first_name': {'S': 'J'},
 739            'new_attr': {'S': 'never_seen_before'},
 740            'date_joined': {'N': '12345'}
 741        }, expects=None)
 742
 743    def test_partial_no_changes(self):
 744        # Unchanged, no save.
 745        with mock.patch.object(self.table, '_update_item', return_value=True) \
 746                as mock_update_item:
 747            # Pretend we loaded it via ``get_item``...
 748            self.johndoe.mark_clean()
 749            self.assertFalse(self.johndoe.partial_save())
 750
 751        self.assertFalse(mock_update_item.called)
 752
 753    def test_partial_with_changes(self):
 754        # Setup the data.
 755        self.table.schema = [
 756            HashKey('username'),
 757        ]
 758
 759        # With changed data.
 760        with mock.patch.object(self.table, '_update_item', return_value=True) \
 761                as mock_update_item:
 762            # Pretend we loaded it via ``get_item``...
 763            self.johndoe.mark_clean()
 764            # Now... MODIFY!!!
 765            self.johndoe['first_name'] = 'J'
 766            self.johndoe['last_name'] = 'Doe'
 767            del self.johndoe['date_joined']
 768            self.assertTrue(self.johndoe.partial_save())
 769            self.assertFalse(self.johndoe.needs_save())
 770
 771        self.assertTrue(mock_update_item.called)
 772        mock_update_item.assert_called_once_with({
 773            'username': 'johndoe',
 774        }, {
 775            'first_name': {
 776                'Action': 'PUT',
 777                'Value': {'S': 'J'},
 778            },
 779            'last_name': {
 780                'Action': 'PUT',
 781                'Value': {'S': 'Doe'},
 782            },
 783            'date_joined': {
 784                'Action': 'DELETE',
 785            }
 786        }, expects={
 787            'first_name': {
 788                'Value': {
 789                    'S': 'John',
 790                },
 791                'Exists': True
 792            },
 793            'last_name': {
 794                'Exists': False
 795            },
 796            'date_joined': {
 797                'Value': {
 798                    'N': '12345',
 799                },
 800                'Exists': True
 801            },
 802        })
 803
 804    def test_delete(self):
 805        # Setup the data.
 806        self.table.schema = [
 807            HashKey('username'),
 808            RangeKey('date_joined'),
 809        ]
 810
 811        with mock.patch.object(self.table, 'delete_item', return_value=True) \
 812                as mock_delete_item:
 813            self.johndoe.delete()
 814
 815        self.assertTrue(mock_delete_item.called)
 816        mock_delete_item.assert_called_once_with(
 817            username='johndoe',
 818            date_joined=12345
 819        )
 820
 821    def test_nonzero(self):
 822        self.assertTrue(self.johndoe)
 823        self.assertFalse(self.create_item({}))
 824
 825
 826class ItemFromItemTestCase(ItemTestCase):
 827    def setUp(self):
 828        super(ItemFromItemTestCase, self).setUp()
 829        self.johndoe = self.create_item(self.johndoe)
 830
 831
 832def fake_results(name, greeting='hello', exclusive_start_key=None, limit=None):
 833    if exclusive_start_key is None:
 834        exclusive_start_key = -1
 835
 836    if limit == 0:
 837        raise Exception("Web Service Returns '400 Bad Request'")
 838
 839    end_cap = 13
 840    results = []
 841    start_key = exclusive_start_key + 1
 842
 843    for i in range(start_key, start_key + 5):
 844        if i < end_cap:
 845            results.append("%s %s #%s" % (greeting, name, i))
 846
 847    # Don't return more than limit results
 848    if limit < len(results):
 849        results = results[:limit]
 850
 851    retval = {
 852        'results': results,
 853    }
 854
 855    if exclusive_start_key + 5 < end_cap:
 856        retval['last_key'] = exclusive_start_key + 5
 857
 858    return retval
 859
 860
 861class ResultSetTestCase(unittest.TestCase):
 862    def setUp(self):
 863        super(ResultSetTestCase, self).setUp()
 864        self.results = ResultSet()
 865        self.result_function = mock.MagicMock(side_effect=fake_results)
 866        self.results.to_call(self.result_function, 'john', greeting='Hello', limit=20)
 867
 868    def test_first_key(self):
 869        self.assertEqual(self.results.first_key, 'exclusive_start_key')
 870
 871    def test_max_page_size_fetch_more(self):
 872        self.results = ResultSet(max_page_size=10)
 873        self.results.to_call(self.result_function, 'john', greeting='Hello')
 874        self.results.fetch_more()
 875        self.result_function.assert_called_with('john', greeting='Hello', limit=10)
 876        self.result_function.reset_mock()
 877
 878    def test_max_page_size_and_smaller_limit_fetch_more(self):
 879        self.results = ResultSet(max_page_size=10)
 880        self.results.to_call(self.result_function, 'john', greeting='Hello', limit=5)
 881        self.results.fetch_more()
 882        self.result_function.assert_called_with('john', greeting='Hello', limit=5)
 883        self.result_function.reset_mock()
 884
 885    def test_max_page_size_and_bigger_limit_fetch_more(self):
 886        self.results = ResultSet(max_page_size=10)
 887        self.results.to_call(self.result_function, 'john', greeting='Hello', limit=15)
 888        self.results.fetch_more()
 889        self.result_function.assert_called_with('john', greeting='Hello', limit=10)
 890        self.result_function.reset_mock()
 891
 892    def test_fetch_more(self):
 893        # First "page".
 894        self.results.fetch_more()
 895        self.assertEqual(self.results._results, [
 896            'Hello john #0',
 897            'Hello john #1',
 898            'Hello john #2',
 899            'Hello john #3',
 900            'Hello john #4',
 901        ])
 902
 903        self.result_function.assert_called_with('john', greeting='Hello', limit=20)
 904        self.result_function.reset_mock()
 905
 906        # Fake in a last key.
 907        self.results._last_key_seen = 4
 908        # Second "page".
 909        self.results.fetch_more()
 910        self.assertEqual(self.results._results, [
 911            'Hello john #5',
 912            'Hello john #6',
 913            'Hello john #7',
 914            'Hello john #8',
 915            'Hello john #9',
 916        ])
 917
 918        self.result_function.assert_called_with('john', greeting='Hello', limit=20, exclusive_start_key=4)
 919        self.result_function.reset_mock()
 920
 921        # Fake in a last key.
 922        self.results._last_key_seen = 9
 923        # Last "page".
 924        self.results.fetch_more()
 925        self.assertEqual(self.results._results, [
 926            'Hello john #10',
 927            'Hello john #11',
 928            'Hello john #12',
 929        ])
 930
 931        # Fake in a key outside the range.
 932        self.results._last_key_seen = 15
 933        # Empty "page". Nothing new gets added
 934        self.results.fetch_more()
 935        self.assertEqual(self.results._results, [])
 936
 937        # Make sure we won't check for results in the future.
 938        self.assertFalse(self.results._results_left)
 939
 940    def test_iteration(self):
 941        # First page.
 942        self.assertEqual(next(self.results), 'Hello john #0')
 943        self.assertEqual(next(self.results), 'Hello john #1')
 944        self.assertEqual(next(self.results), 'Hello john #2')
 945        self.assertEqual(next(self.results), 'Hello john #3')
 946        self.assertEqual(next(self.results), 'Hello john #4')
 947        self.assertEqual(self.results._limit, 15)
 948        # Second page.
 949        self.assertEqual(next(self.results), 'Hello john #5')
 950        self.assertEqual(next(self.results), 'Hello john #6')
 951        self.assertEqual(next(self.results), 'Hello john #7')
 952        self.assertEqual(next(self.results), 'Hello john #8')
 953        self.assertEqual(next(self.results), 'Hello john #9')
 954        self.assertEqual(self.results._limit, 10)
 955        # Third page.
 956        self.assertEqual(next(self.results), 'Hello john #10')
 957        self.assertEqual(next(self.results), 'Hello john #11')
 958        self.assertEqual(next(self.results), 'Hello john #12')
 959        self.assertRaises(StopIteration, self.results.next)
 960        self.assertEqual(self.results._limit, 7)
 961
 962    def test_limit_smaller_than_first_page(self):
 963        results = ResultSet()
 964        results.to_call(fake_results, 'john', greeting='Hello', limit=2)
 965        self.assertEqual(next(results), 'Hello john #0')
 966        self.assertEqual(next(results), 'Hello john #1')
 967        self.assertRaises(StopIteration, results.next)
 968
 969    def test_limit_equals_page(self):
 970        results = ResultSet()
 971        results.to_call(fake_results, 'john', greeting='Hello', limit=5)
 972        # First page
 973        self.assertEqual(next(results), 'Hello john #0')
 974        self.assertEqual(next(results), 'Hello john #1')
 975        self.assertEqual(next(results), 'Hello john #2')
 976        self.assertEqual(next(results), 'Hello john #3')
 977        self.assertEqual(next(results), 'Hello john #4')
 978        self.assertRaises(StopIteration, results.next)
 979
 980    def test_limit_greater_than_page(self):
 981        results = ResultSet()
 982        results.to_call(fake_results, 'john', greeting='Hello', limit=6)
 983        # First page
 984        self.assertEqual(next(results), 'Hello john #0')
 985        self.assertEqual(next(results), 'Hello john #1')
 986        self.assertEqual(next(results), 'Hello john #2')
 987        self.assertEqual(next(results), 'Hello john #3')
 988        self.assertEqual(next(results), 'Hello john #4')
 989        # Second page
 990        self.assertEqual(next(results), 'Hello john #5')
 991        self.assertRaises(StopIteration, results.next)
 992
 993    def test_iteration_noresults(self):
 994        def none(limit=10):
 995            return {
 996                'results': [],
 997            }
 998
 999        results = ResultSet()
1000        results.to_call(none, limit=20)
1001        self.assertRaises(StopIteration, results.next)
1002
1003    def test_iteration_sporadic_pages(self):
1004        # Some pages have no/incomplete results but have a ``LastEvaluatedKey``
1005        # (for instance, scans with filters), so we need to accommodate that.
1006        def sporadic():
1007            # A dict, because Python closures have read-only access to the
1008            # reference itself.
1009            count = {'value': -1}
1010
1011            def _wrapper(limit=10, exclusive_start_key=None):
1012                count['value'] = count['value'] + 1
1013
1014                if count['value'] == 0:
1015                    # Full page.
1016                    return {
1017                        'results': [
1018                            'Result #0',
1019                            'Result #1',
1020                            'Result #2',
1021                            'Result #3',
1022                        ],
1023                        'last_key': 'page-1'
1024                    }
1025                elif count['value'] == 1:
1026                    # Empty page but continue.
1027                    return {
1028                        'results': [],
1029                        'last_key': 'page-2'
1030                    }
1031                elif count['value'] == 2:
1032                    # Final page.
1033                    return {
1034                        'results': [
1035                            'Result #4',
1036                            'Result #5',
1037                            'Result #6',
1038                        ],
1039                    }
1040
1041            return _wrapper
1042
1043        results = ResultSet()
1044        results.to_call(sporadic(), limit=20)
1045        # First page
1046        self.assertEqual(next(results), 'Result #0')
1047        self.assertEqual(next(results), 'Result #1')
1048        self.assertEqual(next(results), 'Result #2')
1049        self.assertEqual(next(results), 'Result #3')
1050        # Second page (misses!)
1051        # Moves on to the third page
1052        self.assertEqual(next(results), 'Result #4')
1053        self.assertEqual(next(results), 'Result #5')
1054        self.assertEqual(next(results), 'Result #6')
1055        self.assertRaises(StopIteration, results.next)
1056
1057    def test_list(self):
1058        self.assertEqual(list(self.results), [
1059            'Hello john #0',
1060            'Hello john #1',
1061            'Hello john #2',
1062            'Hello john #3',
1063            'Hello john #4',
1064            'Hello john #5',
1065            'Hello john #6',
1066            'Hello john #7',
1067            'Hello john #8',
1068            'Hello john #9',
1069            'Hello john #10',
1070            'Hello john #11',
1071            'Hello john #12'
1072        ])
1073
1074
1075def fake_batch_results(keys):
1076    results = []
1077    simulate_unprocessed = True
1078
1079    if len(keys) and keys[0] == 'johndoe':
1080        simulate_unprocessed = False
1081
1082    for key in keys:
1083        if simulate_unprocessed and key == 'johndoe':
1084            continue
1085
1086        results.append("hello %s" % key)
1087
1088    retval = {
1089        'results': results,
1090        'last_key': None,
1091    }
1092
1093    if simulate_unprocessed:
1094        retval['unprocessed_keys'] = ['johndoe']
1095
1096    return retval
1097
1098
1099class BatchGetResultSetTestCase(unittest.TestCase):
1100    def setUp(self):
1101        super(BatchGetResultSetTestCase, self).setUp()
1102        self.results = BatchGetResultSet(keys=[
1103            'alice',
1104            'bob',
1105            'jane',
1106            'johndoe',
1107        ])
1108        self.results.to_call(fake_batch_results)
1109
1110    def test_fetch_more(self):
1111        # First "page".
1112        self.results.fetch_more()
1113        self.assertEqual(self.results._results, [
1114            'hello alice',
1115            'hello bob',
1116            'hello jane',
1117        ])
1118        self.assertEqual(self.results._keys_left, ['johndoe'])
1119
1120        # Second "page".
1121        self.results.fetch_more()
1122        self.assertEqual(self.results._results, [
1123            'hello johndoe',
1124        ])
1125
1126        # Empty "page". Nothing new gets added
1127        self.results.fetch_more()
1128        self.assertEqual(self.results._results, [])
1129
1130        # Make sure we won't check for results in the future.
1131        self.assertFalse(self.results._results_left)
1132
1133    def test_fetch_more_empty(self):
1134        self.results.to_call(lambda keys: {'results': [], 'last_key': None})
1135
1136        self.results.fetch_more()
1137        self.assertEqual(self.results._results, [])
1138        self.assertRaises(StopIteration, self.results.next)
1139
1140    def test_iteration(self):
1141        # First page.
1142        self.assertEqual(next(self.results), 'hello alice')
1143        self.assertEqual(next(self.results), 'hello bob')
1144        self.assertEqual(next(self.results), 'hello jane')
1145        self.assertEqual(next(self.results), 'hello johndoe')
1146        self.assertRaises(StopIteration, self.results.next)
1147
1148
1149class TableTestCase(unittest.TestCase):
1150    def setUp(self):
1151        super(TableTestCase, self).setUp()
1152        self.users = Table('users', connection=FakeDynamoDBConnection())
1153        self.default_connection = DynamoDBConnection(
1154            aws_access_key_id='access_key',
1155            aws_secret_access_key='secret_key'
1156        )
1157
1158    def test__introspect_schema(self):
1159        raw_schema_1 = [
1160            {
1161                "AttributeName": "username",
1162                "KeyType": "HASH"
1163            },
1164            {
1165                "AttributeName": "date_joined",
1166                "KeyType": "RANGE"
1167            }
1168        ]
1169        raw_attributes_1 = [
1170            {
1171                'AttributeName': 'username',
1172                'AttributeType': 'S'
1173            },
1174            {
1175                'AttributeName': 'date_joined',
1176                'AttributeType': 'S'
1177            },
1178        ]
1179        schema_1 = self.users._introspect_schema(raw_schema_1, raw_attributes_1)
1180        self.assertEqual(len(schema_1), 2)
1181        self.assertTrue(isinstance(schema_1[0], HashKey))
1182        self.assertEqual(schema_1[0].name, 'username')
1183        self.assertTrue(isinstance(schema_1[1], RangeKey))
1184        self.assertEqual(schema_1[1].name, 'date_joined')
1185
1186        raw_schema_2 = [
1187            {
1188                "AttributeName": "username",
1189                "KeyType": "BTREE"
1190            },
1191        ]
1192        raw_attributes_2 = [
1193            {
1194                'AttributeName': 'username',
1195                'AttributeType': 'S'
1196            },
1197        ]
1198        self.assertRaises(
1199            exceptions.UnknownSchemaFieldError,
1200            self.users._introspect_schema,
1201            raw_schema_2,
1202            raw_attributes_2
1203        )
1204
1205        # Test a complex schema & ensure the types come back correctly.
1206        raw_schema_3 = [
1207            {
1208                "AttributeName": "user_id",
1209                "KeyType": "HASH"
1210            },
1211            {
1212                "AttributeName": "junk",
1213                "KeyType": "RANGE"
1214            }
1215        ]
1216        raw_attributes_3 = [
1217            {
1218                'AttributeName': 'user_id',
1219                'AttributeType': 'N'
1220            },
1221            {
1222                'AttributeName': 'junk',
1223                'AttributeType': 'B'
1224            },
1225        ]
1226        schema_3 = self.users._introspect_schema(raw_schema_3, raw_attributes_3)
1227        self.assertEqual(len(schema_3), 2)
1228        self.assertTrue(isinstance(schema_3[0], HashKey))
1229        self.assertEqual(schema_3[0].name, 'user_id')
1230        self.assertEqual(schema_3[0].data_type, NUMBER)
1231        self.assertTrue(isinstance(schema_3[1], RangeKey))
1232        self.assertEqual(schema_3[1].name, 'junk')
1233        self.assertEqual(schema_3[1].data_type, BINARY)
1234
1235    def test__introspect_indexes(self):
1236        raw_indexes_1 = [
1237            {
1238                "IndexName": "MostRecentlyJoinedIndex",
1239                "KeySchema": [
1240                    {
1241                        "AttributeName": "username",
1242                        "KeyType": "HASH"
1243                    },
1244                    {
1245                        "AttributeName": "date_joined",
1246                        "KeyType": "RANGE"
1247                    }
1248                ],
1249                "Projection": {
1250                    "ProjectionType": "KEYS_ONLY"
1251                }
1252            },
1253            {
1254                "IndexName": "EverybodyIndex",
1255                "KeySchema": [
1256                    {
1257                        "AttributeName": "username",
1258                        "KeyType": "HASH"
1259                    },
1260                ],
1261                "Projection": {
1262                    "ProjectionType": "ALL"
1263                }
1264            },
1265            {
1266                "IndexName": "GenderIndex",
1267                "KeySchema": [
1268                    {
1269                        "AttributeName": "username",
1270                        "KeyType": "HASH"
1271                    },
1272                    {
1273                        "AttributeName": "date_joined",
1274                        "KeyType": "RANGE"
1275                    }
1276                ],
1277                "Projection": {
1278                    "ProjectionType": "INCLUDE",
1279                    "NonKeyAttributes": [
1280                        'gender',
1281                    ]
1282                }
1283            }
1284        ]
1285        indexes_1 = self.users._introspect_indexes(raw_indexes_1)
1286        self.assertEqual(len(indexes_1), 3)
1287        self.assertTrue(isinstance(indexes_1[0], KeysOnlyIndex))
1288        self.assertEqual(indexes_1[0].name, 'MostRecentlyJoinedIndex')
1289        self.assertEqual(len(indexes_1[0].parts), 2)
1290        self.assertTrue(isinstance(indexes_1[1], AllIndex))
1291        self.assertEqual(indexes_1[1].name, 'EverybodyIndex')
1292        self.assertEqual(len(indexes_1[1].parts), 1)
1293        self.assertTrue(isinstance(indexes_1[2], IncludeIndex))
1294        self.assertEqual(indexes_1[2].name, 'GenderIndex')
1295        self.assertEqual(len(indexes_1[2].parts), 2)
1296        self.assertEqual(indexes_1[2].includes_fields, ['gender'])
1297
1298        raw_indexes_2 = [
1299            {
1300                "IndexName": "MostRecentlyJoinedIndex",
1301                "KeySchema": [
1302                    {
1303                        "AttributeName": "username",
1304                        "KeyType": "HASH"
1305                    },
1306                    {
1307                        "AttributeName": "date_joined",
1308                        "KeyType": "RANGE"
1309                    }
1310                ],
1311                "Projection": {
1312                    "ProjectionType": "SOMETHING_CRAZY"
1313                }
1314            },
1315        ]
1316        self.assertRaises(
1317            exceptions.UnknownIndexFieldError,
1318            self.users._introspect_indexes,
1319            raw_indexes_2
1320        )
1321
1322    def test_initialization(self):
1323        users = Table('users', connection=self.default_connection)
1324        self.assertEqual(users.table_name, 'users')
1325        self.assertTrue(isinstance(users.connection, DynamoDBConnection))
1326        self.assertEqual(users.throughput['read'], 5)
1327        self.assertEqual(users.throughput['write'], 5)
1328        self.assertEqual(users.schema, None)
1329        self.assertEqual(users.indexes, None)
1330
1331        groups = Table('groups', connection=FakeDynamoDBConnection())
1332        self.assertEqual(groups.table_name, 'groups')
1333        self.assertTrue(hasattr(groups.connection, 'assert_called_once_with'))
1334
1335    def test_create_simple(self):
1336        conn = FakeDynamoDBConnection()
1337
1338        with mock.patch.object(conn, 'create_table', return_value={}) \
1339                as mock_create_table:
1340            retval = Table.create('users', schema=[
1341                HashKey('username'),
1342                RangeKey('date_joined', data_type=NUMBER)
1343            ], connection=conn)
1344            self.assertTrue(retval)
1345
1346        self.assertTrue(mock_create_table.called)
1347        mock_create_table.assert_called_once_with(attribute_definitions=[
1348            {
1349                'AttributeName': 'username',
1350                'AttributeType': 'S'
1351            },
1352            {
1353                'AttributeName': 'date_joined',
1354                'AttributeType': 'N'
1355            }
1356        ],
1357        table_name='users',
1358        key_schema=[
1359            {
1360                'KeyType': 'HASH',
1361                'AttributeName': 'username'
1362            },
1363            {
1364                'KeyType': 'RANGE',
1365                'AttributeName': 'date_joined'
1366            }
1367        ],
1368        provisioned_throughput={
1369            'WriteCapacityUnits': 5,
1370            'ReadCapacityUnits': 5
1371        })
1372
1373    def test_create_full(self):
1374        conn = FakeDynamoDBConnection()
1375
1376        with mock.patch.object(conn, 'create_table', return_value={}) \
1377                as mock_create_table:
1378            retval = Table.create('users', schema=[
1379                HashKey('username'),
1380                RangeKey('date_joined', data_type=NUMBER)
1381            ], throughput={
1382                'read':20,
1383                'write': 10,
1384            }, indexes=[
1385                KeysOnlyIndex('FriendCountIndex', parts=[
1386                    RangeKey('friend_count')
1387                ]),
1388            ], global_indexes=[
1389                GlobalKeysOnlyIndex('FullFriendCountIndex', parts=[
1390                    RangeKey('friend_count')
1391                ], throughput={
1392                    'read': 10,
1393                    'write': 8,
1394                }),
1395            ], connection=conn)
1396            self.assertTrue(retval)
1397
1398        self.assertTrue(mock_create_table.called)
1399        mock_create_table.assert_called_once_with(attribute_definitions=[
1400            {
1401                'AttributeName': 'username',
1402                'AttributeType': 'S'
1403            },
1404            {
1405                'AttributeName': 'date_joined',
1406                'AttributeType': 'N'
1407            },
1408            {
1409                'AttributeName': 'friend_count',
1410                'AttributeType': 'S'
1411            }
1412        ],
1413        key_schema=[
1414            {
1415                'KeyType': 'HASH',
1416                'AttributeName': 'username'
1417            },
1418            {
1419                'KeyType': 'RANGE',
1420                'AttributeName': 'date_joined'
1421            }
1422        ],
1423        table_name='users',
1424        provisioned_throughput={
1425            'WriteCapacityUnits': 10,
1426            'ReadCapacityUnits': 20
1427        },
1428        global_secondary_indexes=[
1429            {
1430                'KeySchema': [
1431                    {
1432                        'KeyType': 'RANGE',
1433                        'AttributeName': 'friend_count'
1434                    }
1435                ],
1436                'IndexName': 'FullFriendCountIndex',
1437                'Projection': {
1438                    'ProjectionType': 'KEYS_ONLY'
1439                },
1440                'ProvisionedThroughput': {
1441                    'WriteCapacityUnits': 8,
1442                    'ReadCapacityUnits': 10
1443                }
1444            }
1445        ],
1446        local_secondary_indexes=[
1447            {
1448                'KeySchema': [
1449                    {
1450                        'KeyType': 'RANGE',
1451                        'AttributeName': 'friend_count'
1452                    }
1453                ],
1454                'IndexName': 'FriendCountIndex',
1455                'Projection': {
1456                    'ProjectionType': 'KEYS_ONLY'
1457                }
1458            }
1459        ])
1460
1461    def test_describe(self):
1462        expected = {
1463            "Table": {
1464                "AttributeDefinitions": [
1465                    {
1466                        "AttributeName": "username",
1467                        "AttributeType": "S"
1468                    }
1469                ],
1470                "ItemCount": 5,
1471                "KeySchema": [
1472                    {
1473                        "AttributeName": "username",
1474                        "KeyType": "HASH"
1475                    }
1476                ],
1477                "LocalSecondaryIndexes": [
1478                    {
1479                        "IndexName": "UsernameIndex",
1480                        "KeySchema": [
1481                            {
1482                                "AttributeName": "username",
1483                                "KeyType": "HASH"
1484                            }
1485                        ],
1486                        "Projection": {
1487                            "ProjectionType": "KEYS_ONLY"
1488                        }
1489                    }
1490                ],
1491                "ProvisionedThroughput": {
1492                    "ReadCapacityUnits": 20,
1493                    "WriteCapacityUnits": 6
1494                },
1495                "TableName": "Thread",
1496                "TableStatus": "ACTIVE"
1497            }
1498        }
1499
1500        with mock.patch.object(
1501                self.users.connection,
1502                'describe_table',
1503                return_value=expected) as mock_describe:
1504            self.assertEqual(self.users.throughput['read'], 5)
1505            self.assertEqual(self.users.throughput['write'], 5)
1506            self.assertEqual(self.users.schema, None)
1507            self.assertEqual(self.users.indexes, None)
1508
1509            self.users.describe()
1510
1511            self.assertEqual(self.users.throughput['read'], 20)
1512            self.assertEqual(self.users.throughput['write'], 6)
1513            self.assertEqual(len(self.users.schema), 1)
1514            self.assertEqual(isinstance(self.users.schema[0], HashKey), 1)
1515            self.assertEqual(len(self.users.indexes), 1)
1516
1517        mock_describe.assert_called_once_with('users')
1518
1519    def test_update(self):
1520        with mock.patch.object(
1521                self.users.connection,
1522                'update_table',
1523                return_value={}) as mock_update:
1524            self.assertEqual(self.users.throughput['read'], 5)
1525            self.assertEqual(self.users.throughput['write'], 5)
1526            self.users.update(throughput={
1527                'read': 7,
1528                'write': 2,
1529            })
1530            self.assertEqual(self.users.throughput['read'], 7)
1531            self.assertEqual(self.users.throughput['write'], 2)
1532
1533        mock_update.assert_called_once_with(
1534            'users',
1535            global_secondary_index_updates=None,
1536            provisioned_throughput={
1537                'WriteCapacityUnits': 2,
1538                'ReadCapacityUnits': 7
1539            }
1540        )
1541
1542        with mock.patch.object(
1543                self.users.connection,
1544                'update_table',
1545                return_value={}) as mock_update:
1546            self.assertEqual(self.users.throughput['read'], 7)
1547            self.assertEqual(self.users.throughput['write'], 2)
1548            self.users.update(throughput={
1549                'read': 9,
1550                'write': 5,
1551            },
1552            global_indexes={
1553                'WhateverIndex': {
1554                    'read': 6,
1555                    'write': 1
1556                },
1557                'AnotherIndex': {
1558                    'read': 1,
1559                    'write': 2
1560                }
1561            })
1562            self.assertEqual(self.users.throughput['read'], 9)
1563            self.assertEqual(self.users.throughput['write'], 5)
1564
1565        args, kwargs = mock_update.call_args
1566        self.assertEqual(args, ('users',))
1567        self.assertEqual(kwargs['provisioned_throughput'], {
1568            'WriteCapacityUnits': 5,
1569            'ReadCapacityUnits': 9,
1570            })
1571        update = kwargs['global_secondary_index_updates'][:]
1572        update.sort(key=lambda x: x['Update']['IndexName'])
1573        self.assertDictEqual(
1574            update[0],
1575            {
1576                'Update': {
1577                    'IndexName': 'AnotherIndex',
1578                    'ProvisionedThroughput': {
1579                        'WriteCapacityUnits': 2,
1580                        'ReadCapacityUnits': 1
1581                    }
1582                }
1583            })
1584        self.assertDictEqual(
1585            update[1],
1586            {
1587                'Update': {
1588                    'IndexName': 'WhateverIndex',
1589                    'ProvisionedThroughput': {
1590                        'WriteCapacityUnits': 1,
1591                        'ReadCapacityUnits': 6
1592                    }
1593                }
1594            })
1595
1596    def test_create_global_secondary_index(self):
1597        with mock.patch.object(
1598                self.users.connection,
1599                'update_table',
1600                return_value={}) as mock_update:
1601            self.users.create_global_secondary_index(
1602                global_index=GlobalAllIndex(
1603                    'JustCreatedIndex',
1604                    parts=[
1605                        HashKey('requiredHashKey')
1606                    ],
1607                    throughput={
1608                        'read': 2,
1609                        'write': 2
1610                    }
1611                )
1612            )
1613
1614        mock_update.assert_called_once_with(
1615            'users',
1616            global_secondary_index_updates=[
1617                {
1618                    'Create': {
1619                        'IndexName': 'JustCreatedIndex',
1620                        'KeySchema': [
1621                            {
1622                                'KeyType': 'HASH',
1623                                'AttributeName': 'requiredHashKey'
1624                            }
1625                        ],
1626                        'Projection': {
1627                            'ProjectionType': 'ALL'
1628                        },
1629                        'ProvisionedThroughput': {
1630                            'WriteCapacityUnits': 2,
1631                            'ReadCapacityUnits': 2
1632                        }
1633                    }
1634                }
1635            ],
1636            attribute_definitions=[
1637                {
1638                    'AttributeName': 'requiredHashKey',
1639                    'AttributeType': 'S'
1640                }
1641            ]
1642        )
1643
1644    def test_delete_global_secondary_index(self):
1645        with mock.patch.object(
1646                self.users.connection,
1647                'update_table',
1648                return_value={}) as mock_update:
1649            self.users.delete_global_secondary_index('RandomGSIIndex')
1650
1651        mock_update.assert_called_once_with(
1652            'users',
1653            global_secondary_index_updates=[
1654                {
1655                    'Delete': {
1656                        'IndexName': 'RandomGSIIndex',
1657                    }
1658                }
1659            ]
1660        )
1661
1662    def test_update_global_secondary_index(self):
1663        # Updating a single global secondary index
1664        with mock.patch.object(
1665                self.users.connection,
1666                'update_table',
1667                return_value={}) as mock_update:
1668            self.users.update_global_secondary_index(global_indexes={
1669                'A_IndexToBeUpdated': {
1670                    'read': 5,
1671                    'write': 5
1672                }
1673            })
1674
1675        mock_update.assert_called_once_with(
1676            'users',
1677            global_secondary_index_updates=[
1678                {
1679                    'Update': {
1680                        'IndexName': 'A_IndexToBeUpdated',
1681                        "ProvisionedThroughput": {
1682                            "ReadCapacityUnits": 5,
1683                            "WriteCapacityUnits": 5
1684                        },
1685                    }
1686                }
1687            ]
1688        )
1689
1690        # Updating multiple global secondary indexes
1691        with mock.patch.object(
1692                self.users.connection,
1693                'update_table',
1694                return_value={}) as mock_update:
1695            self.users.update_global_secondary_index(global_indexes={
1696                'A_IndexToBeUpdated': {
1697                    'read': 5,
1698                    'write': 5
1699                },
1700                'B_IndexToBeUpdated': {
1701                    'read': 9,
1702                    'write': 9
1703                }
1704            })
1705
1706        args, kwargs = mock_update.call_args
1707        self.assertEqual(args, ('users',))
1708        update = kwargs['global_secondary_index_updates'][:]
1709        update.sort(key=lambda x: x['Update']['IndexName'])
1710        self.assertDictEqual(
1711            update[0],
1712            {
1713                'Update': {
1714                    'IndexName': 'A_IndexToBeUpdated',
1715                    'ProvisionedThroughput': {
1716                        'WriteCapacityUnits': 5,
1717                        'ReadCapacityUnits': 5
1718                    }
1719                }
1720            })
1721        self.assertDictEqual(
1722            update[1],
1723            {
1724                'Update': {
1725                    'IndexName': 'B_IndexToBeUpdated',
1726                    'ProvisionedThroughput': {
1727                        'WriteCapacityUnits': 9,
1728                        'ReadCapacityUnits': 9
1729                    }
1730                }
1731            })
1732
1733    def test_delete(self):
1734        with mock.patch.object(
1735                self.users.connection,
1736                'delete_table',
1737                return_value={}) as mock_delete:
1738            self.assertTrue(self.users.delete())
1739
1740        mock_delete.assert_called_once_with('users')
1741
1742    def test_get_item(self):
1743        expected = {
1744            'Item': {
1745                'username': {'S': 'johndoe'},
1746                'first_name': {'S': 'John'},
1747                'last_name': {'S': 'Doe'},
1748                'date_joined': {'N': '1366056668'},
1749                'friend_count': {'N': '3'},
1750                'friends': {'SS': ['alice', 'bob', 'jane']},
1751            }
1752        }
1753
1754        with mock.patch.object(
1755                self.users.connection,
1756                'get_item',
1757                return_value=expected) as mock_get_item:
1758            item = self.users.get_item(username='johndoe')
1759            self.assertEqual(item['username'], 'johndoe')
1760            self.assertEqual(item['first_name'], 'John')
1761
1762        mock_get_item.assert_called_once_with('users', {
1763            'username': {'S': 'johndoe'}
1764        }, consistent_read=False, attributes_to_get=None)
1765
1766        with mock.patch.object(
1767                self.users.connection,
1768                'get_item',
1769                return_value=expected) as mock_get_item:
1770            item = self.users.get_item(username='johndoe', attributes=[
1771                'username',
1772                'first_name',
1773            ])
1774
1775        mock_get_item.assert_called_once_with('users', {
1776            'username': {'S': 'johndoe'}
1777        }, consistent_read=False, attributes_to_get=['username', 'first_name'])
1778
1779    def test_has_item(self):
1780        expected = {
1781            'Item': {
1782                'username': {'S': 'johndoe'},
1783                'first_name': {'S': 'John'},
1784                'last_name': {'S': 'Doe'},
1785                'date_joined': {'N': '1366056668'},
1786                'friend_count': {'N': '3'},
1787                'friends': {'SS': ['alice', 'bob', 'jane']},
1788            }
1789        }
1790
1791        with mock.patch.object(
1792                self.users.connection,
1793                'get_item',
1794                return_value=expected) as mock_get_item:
1795            found = self.users.has_item(username='johndoe')
1796            self.assertTrue(found)
1797
1798        with mock.patch.object(
1799                self.users.connection,
1800                'get_item') as mock_get_item:
1801            mock_get_item.side_effect = JSONResponseError("Nope.", None, None)
1802            found = self.users.has_item(username='mrsmith')
1803            self.assertFalse(found)
1804
1805    def test_lookup_hash(self):
1806        """Tests the "lookup" function with just a hash key"""
1807        expected = {
1808            'Item': {
1809                'username': {'S': 'johndoe'},
1810                'first_name': {'S': 'John'},
1811                'last_name': {'S': 'Doe'},
1812                'date_joined': {'N': '1366056668'},
1813                'friend_count': {'N': '3'},
1814                'friends': {'SS': ['alice', 'bob', 'jane']},
1815            }
1816        }
1817
1818        # Set the Schema
1819        self.users.schema = [
1820            HashKey('username'),
1821            RangeKey('date_joined', data_type=NUMBER),
1822        ]
1823
1824        with mock.patch.object(
1825                self.users,
1826                'get_item',
1827                return_value=expected) as mock_get_item:
1828            self.users.lookup('johndoe')
1829
1830        mock_get_item.assert_called_once_with(
1831            username= 'johndoe')
1832
1833    def test_lookup_hash_and_range(self):
1834        """Test the "lookup" function with a hash and range key"""
1835        expected = {
1836            'Item': {
1837                'username': {'S': 'johndoe'},
1838                'first_name': {'S': 'John'},
1839                'last_name': {'S': 'Doe'},
1840                'date_joined': {'N': '1366056668'},
1841                'friend_count': {'N': '3'},
1842                'friends': {'SS': ['alice', 'bob', 'jane']},
1843            }
1844        }
1845
1846        # Set the Schema
1847        self.users.schema = [
1848            HashKey('username'),
1849            RangeKey('date_joined', data_type=NUMBER),
1850        ]
1851
1852        with mock.patch.object(
1853                self.users,
1854                'get_item',
1855                return_value=expected) as mock_get_item:
1856            self.users.lookup('johndoe', 1366056668)
1857
1858        mock_get_item.assert_called_once_with(
1859            username= 'johndoe',
1860            date_joined= 1366056668)
1861
1862    def test_put_item(self):
1863        with mock.patch.object(
1864                self.users.connection,
1865                'put_item',
1866                return_value={}) as mock_put_item:
1867            self.users.put_item(data={
1868                'username': 'johndoe',
1869                'last_name': 'Doe',
1870                'date_joined': 12345,
1871            })
1872
1873        mock_put_item.assert_called_once_with('users', {
1874            'username': {'S': 'johndoe'},
1875            'last_name': {'S': 'Doe'},
1876            'date_joined': {'N': '12345'}
1877        }, expected={
1878            'username': {
1879                'Exists': False,
1880            },
1881            'last_name': {
1882                'Exists': False,
1883            },
1884            'date_joined': {
1885                'Exists': False,
1886            }
1887        })
1888
1889    def test_private_put_item(self):
1890        with mock.patch.object(
1891                self.users.connection,
1892                'put_item',
1893                return_value={}) as mock_put_item:
1894            self.users._put_item({'some': 'data'})
1895
1896        mock_put_item.assert_called_once_with('users', {'some': 'data'})
1897
1898    def test_private_update_item(self):
1899        with mock.patch.object(
1900                self.users.connection,
1901                'update_item',
1902                return_value={}) as mock_update_item:
1903            self.users._update_item({
1904                'username': 'johndoe'
1905            }, {
1906                'some': 'data',
1907            })
1908
1909        mock_update_item.assert_called_once_with('users', {
1910            'username': {'S': 'johndoe'},
1911        }, {
1912            'some': 'data',
1913        })
1914
1915    def test_delete_item(self):
1916        with mock.patch.object(
1917                self.users.connection,
1918                'delete_item',
1919                return_value={}) as mock_delete_item:
1920            self.assertTrue(self.users.delete_item(username='johndoe', date_joined=23456))
1921
1922        mock_delete_item.assert_called_once_with('users', {
1923            'username': {
1924                'S': 'johndoe'
1925            },
1926            'date_joined': {
1927                'N': '23456'
1928            }
1929        }, expected=None, conditional_operator=None)
1930
1931    def test_delete_item_conditionally(self):
1932        with mock.patch.object(
1933                self.users.connection,
1934                'delete_item',
1935                return_value={}) as mock_delete_item:
1936            self.assertTrue(self.users.delete_item(expected={'balance__eq': 0},
1937                                                   username='johndoe', date_joined=23456))
1938
1939        mock_delete_item.assert_called_once_with('users', {
1940            'username': {
1941                'S': 'johndoe'
1942            },
1943            'date_joined': {
1944                'N': '23456'
1945            }
1946        },
1947        expected={
1948            'balance': {
1949                'ComparisonOperator': 'EQ', 'AttributeValueList': [{'N': '0'}]
1950                },
1951            },
1952        conditional_operator=None)
1953
1954        def side_effect(*args, **kwargs):
1955            raise exceptions.ConditionalCheckFailedException(400, '', {})
1956
1957        with mock.patch.object(
1958                self.users.connection,
1959                'delete_item',
1960                side_effect=side_effect) as mock_delete_item:
1961            self.assertFalse(self.users.delete_item(expected={'balance__eq': 0},
1962                                                    username='johndoe', date_joined=23456))
1963
1964    def test_get_key_fields_no_schema_populated(self):
1965        expected = {
1966            "Table": {
1967                "AttributeDefinitions": [
1968                    {
1969                        "AttributeName": "username",
1970                        "AttributeType": "S"
1971                    },
1972                    {
1973                        "AttributeName": "date_joined",
1974                        "AttributeType": "N"
1975                    }
1976                ],
1977                "ItemCount": 5,
1978                "KeySchema": [
1979                    {
1980                        "AttributeName": "username",
1981                        "KeyType": "HASH"
1982                    },
1983                    {
1984                        "AttributeName": "date_joined",
1985                        "KeyType": "RANGE"
1986                    }
1987                ],
1988                "LocalSecondaryIndexes": [
1989                    {
1990                        "IndexName": "UsernameIndex",
1991                        "KeySchema": [
1992                            {
1993                                "AttributeName": "username",
1994                                "KeyType": "HASH"
1995                            }
1996                        ],
1997                        "Projection": {
1998                            "ProjectionType": "KEYS_ONLY"
1999                        }
2000                    }
2001                ],
2002                "ProvisionedThroughput": {
2003                    "ReadCapacityUnits": 20,
2004                    "WriteCapacityUnits": 6
2005                },
2006                "TableName": "Thread",
2007                "TableStatus": "ACTIVE"
2008            }
2009        }
2010
2011        with mock.patch.object(
2012                self.users.connection,
2013                'describe_table',
2014                return_value=expected) as mock_describe:
2015            self.assertEqual(self.users.schema, None)
2016
2017            key_fields = self.users.get_key_fields()
2018            self.assertEqual(key_fields, ['username', 'date_joined'])
2019
2020            self.assertEqual(len(self.users.schema), 2)
2021
2022        mock_describe.assert_called_once_with('users')
2023
2024    def test_batch_write_no_writes(self):
2025        with mock.patch.object(
2026                self.users.connection,
2027                'batch_write_item',
2028                return_value={}) as mock_batch:
2029            with self.users.batch_write() as batch:
2030                pass
2031
2032        self.assertFalse(mock_batch.called)
2033
2034    def test_batch_write(self):
2035        with mock.patch.object(
2036                self.users.connection,
2037                'batch_write_item',
2038                return_value={}) as mock_batch:
2039            with self.users.batch_write() as batch:
2040                batch.put_item(data={
2041                    'username': 'jane',
2042                    'date_joined': 12342547
2043                })
2044                batch.delete_item(username='johndoe')
2045                batch.put_item(data={
2046                    'username': 'alice',
2047                    'date_joined': 12342888
2048                })
2049
2050        mock_batch.assert_called_once_with({
2051            'users': [
2052                {
2053                    'PutRequest': {
2054                        'Item': {
2055                            'username': {'S': 'jane'},
2056                            'date_joined': {'N': '12342547'}
2057                        }
2058                    }
2059                },
2060                {
2061                    'PutRequest': {
2062                        'Item': {
2063                            'username': {'S': 'alice'},
2064                            'date_joined': {'N': '12342888'}
2065                        }
2066                    }
2067                },
2068                {
2069                    'DeleteRequest': {
2070                        'Key': {
2071                            'username': {'S': 'johndoe'},
2072                        }
2073                    }
2074                },
2075            ]
2076        })
2077
2078    def test_batch_write_dont_swallow_exceptions(self):
2079        with mock.patch.object(
2080                self.users.connection,
2081                'batch_write_item',
2082                return_value={}) as mock_batch:
2083            try:
2084                with self.users.batch_write() as batch:
2085                    raise Exception('OH NOES')
2086            except Exception as e:
2087                self.assertEqual(str(e), 'OH NOES')
2088
2089        self.assertFalse(mock_batch.called)
2090
2091    def test_batch_write_flushing(self):
2092        with mock.patch.object(
2093                self.users.connection,
2094                'batch_write_item',
2095                return_value={}) as mock_batch:
2096            with self.users.batch_write() as batch:
2097                batch.put_item(data={
2098                    'username': 'jane',
2099                    'date_joined': 12342547
2100                })
2101                # This would only be enough for one batch.
2102                batch.delete_item(username='johndoe1')
2103                batch.delete_item(username='johndoe2')
2104                batch.delete_item(username='johndoe3')
2105                batch.delete_item(username='johndoe4')
2106                batch.delete_item(username='johndoe5')
2107                batch.delete_item(username='johndoe6')
2108                batch.delete_item(username='johndoe7')
2109                batch.delete_item(username='johndoe8')
2110                batch.delete_item(username='johndoe9')
2111                batch.delete_item(username='johndoe10')
2112                batch.delete_item(username='johndoe11')
2113                batch.delete_item(username='johndoe12')
2114                batch.delete_item(username='johndoe13')
2115                batch.delete_item(username='johndoe14')
2116                batch.delete_item(username='johndoe15')
2117                batch.delete_item(username='johndoe16')
2118                batch.delete_item(username='johndoe17')
2119                batch.delete_item(username='johndoe18')
2120                batch.delete_item(username='johndoe19')
2121                batch.delete_item(username='johndoe20')
2122                batch.delete_item(username='johndoe21')
2123                batch.delete_item(username='johndoe22')
2124                batch.delete_item(username='johndoe23')
2125
2126                # We're only at 24 items. No flushing yet.
2127                self.assertEqual(mock_batch.call_count, 0)
2128
2129                # This pushes it over the edge. A flush happens then we start
2130                # queuing objects again.
2131                batch.delete_item(username='johndoe24')
2132                self.assertEqual(mock_batch.call_count, 1)
2133                # Since we add another, there's enough for a second call to
2134                # flush.
2135                batch.delete_item(username='johndoe25')
2136
2137        self.assertEqual(mock_batch.call_count, 2)
2138
2139    def test_batch_write_unprocessed_items(self):
2140        unprocessed = {
2141            'UnprocessedItems': {
2142                'users': [
2143                    {
2144                        'PutRequest': {
2145                            'username': {
2146                                'S': 'jane',
2147                            },
2148                            'date_joined': {
2149                                'N': 12342547
2150                            }
2151                        },
2152                    },
2153                ],
2154            },
2155        }
2156
2157        # Test enqueuing the unprocessed bits.
2158        with mock.patch.object(
2159                self.users.connection,
2160                'batch_write_item',
2161                return_value=unprocessed) as mock_batch:
2162            with self.users.batch_write() as batch:
2163                self.assertEqual(len(batch._unprocessed), 0)
2164
2165                # Trash the ``resend_unprocessed`` method so that we don't
2166                # infinite loop forever here.
2167                batch.resend_unprocessed = lambda: True
2168
2169                batch.put_item(data={
2170                    'username': 'jane',
2171                    'date_joined': 12342547
2172                })
2173                batch.delete_item(username='johndoe')
2174                batch.put_item(data={
2175                    'username': 'alice',
2176                    'date_joined': 12342888
2177                })
2178
2179            self.assertEqual(len(batch._unprocessed), 1)
2180
2181        # Now test resending those unprocessed items.
2182        with mock.patch.object(
2183                self.users.connection,
2184                'batch_write_item',
2185                return_value={}) as mock_batch:
2186            with self.users.batch_write() as batch:
2187                self.assertEqual(len(batch._unprocessed), 0)
2188
2189                # Toss in faked unprocessed items, as though a previous batch
2190                # had failed.
2191                batch._unprocessed = [
2192                    {
2193                        'PutRequest': {
2194                            'username': {
2195                                'S': 'jane',
2196                            },
2197                            'date_joined': {
2198                                'N': 12342547
2199                            }
2200                        },
2201                    },
2202                ]
2203
2204                batch.put_item(data={
2205                    'username': 'jane',
2206                    'date_joined': 12342547
2207                })
2208                batch.delete_item(username='johndoe')
2209                batch.put_item(data={
2210                    'username': 'alice',
2211                    'date_joined': 12342888
2212                })
2213
2214                # Flush, to make sure everything has been processed.
2215                # Unprocessed items should still be hanging around.
2216                batch.flush()
2217                self.assertEqual(len(batch._unprocessed), 1)
2218
2219            # Post-exit, this should be emptied.
2220            self.assertEqual(len(batch._unprocessed), 0)
2221
2222    def test__build_filters(self):
2223        filters = self.users._build_filters({
2224            'username__eq': 'johndoe',
2225            'date_joined__gte': 1234567,
2226            'age__in': [30, 31, 32, 33],
2227            'last_name__between': ['danzig', 'only'],
2228            'first_name__null': False,
2229            'gender__null': True,
2230        }, using=FILTER_OPERATORS)
2231        self.assertEqual(filters, {
2232            'username': {
2233                'AttributeValueList': [
2234                    {
2235                        'S': 'johndoe',
2236                    },
2237                ],
2238                'ComparisonOperator': 'EQ',
2239            },
2240            'date_joined': {
2241                'AttributeValueList': [
2242                    {
2243                        'N': '1234567',
2244                    },
2245                ],
2246                'ComparisonOperator': 'GE',
2247            },
2248            'age': {
2249                'AttributeValueList': [
2250                    {'N': '30'},
2251                    {'N': '31'},
2252                    {'N': '32'},
2253                    {'N': '33'},
2254                ],
2255                'ComparisonOperator': 'IN',
2256            },
2257            'last_name': {
2258                'AttributeValueList': [{'S': 'danzig'}, {'S': 'only'}],
2259                'ComparisonOperator': 'BETWEEN',
2260            },
2261            'first_name': {
2262                'ComparisonOperator': 'NOT_NULL'
2263            },
2264            'gender': {
2265                'ComparisonOperator': 'NULL'
2266            },
2267        })
2268
2269        self.assertRaises(exceptions.UnknownFilterTypeError,
2270            self.users._build_filters,
2271            {
2272                'darling__die': True,
2273            }
2274        )
2275
2276        q_filters = self.users._build_filters({
2277            'username__eq': 'johndoe',
2278            'date_joined__gte': 1234567,
2279            'last_name__between': ['danzig', 'only'],
2280            'gender__beginswith': 'm',
2281        }, using=QUERY_OPERATORS)
2282        self.assertEqual(q_filters, {
2283            'username': {
2284                'AttributeValueList': [
2285                    {
2286                        'S': 'johndoe',
2287                    },
2288                ],
2289                'ComparisonOperator': 'EQ',
2290            },
2291            'date_joined': {
2292                'AttributeValueList': [
2293                    {
2294                        'N': '1234567',
2295                    },
2296                ],
2297                'ComparisonOperator': 'GE',
2298            },
2299            'last_name': {
2300                'AttributeValueList': [{'S': 'danzig'}, {'S': 'only'}],
2301                'ComparisonOperator': 'BETWEEN',
2302            },
2303            'gender': {
2304                'AttributeValueList': [{'S': 'm'}],
2305                'ComparisonOperator': 'BEGINS_WITH',
2306            },
2307        })
2308
2309        self.assertRaises(exceptions.UnknownFilterTypeError,
2310            self.users._build_filters,
2311            {
2312                'darling__die': True,
2313            },
2314            using=QUERY_OPERATORS
2315        )
2316        self.assertRaises(exceptions.UnknownFilterTypeError,
2317            self.users._build_filters,
2318            {
2319                'first_name__null': True,
2320            },
2321            using=QUERY_OPERATORS
2322        )
2323
2324    def test_private_query(self):
2325        expected = {
2326            "ConsumedCapacity": {
2327                "CapacityUnits": 0.5,
2328                "TableName": "users"
2329            },
2330            "Count": 4,
2331            "Items": [
2332                {
2333                    'username': {'S': 'johndoe'},
2334                    'first_name': {'S': 'John'},
2335                    'last_name': {'S': 'Doe'},
2336                    'date_joined': {'N': '1366056668'},
2337                    'friend_count': {'N': '3'},
2338                    'friends': {'SS': ['alice', 'bob', 'jane']},
2339                },
2340                {
2341                    'username': {'S': 'jane'},
2342                    'first_name': {'S': 'Jane'},
2343                    'last_name': {'S': 'Doe'},
2344                    'date_joined': {'N': '1366057777'},
2345                    'friend_count': {'N': '2'},
2346                    'friends': {'SS': ['alice', 'johndoe']},
2347                },
2348                {
2349                    'username': {'S': 'alice'},
2350                    'first_name': {'S': 'Alice'},
2351                    'last_name': {'S': 'Expert'},
2352                    'date_joined': {'N': '1366056680'},
2353                    'friend_count': {'N': '1'},
2354                    'friends': {'SS': ['jane']},
2355                },
2356                {
2357                    'username': {'S': 'bob'},
2358                    'first_name': {'S': 'Bob'},
2359                    'last_name': {'S': 'Smith'},
2360                    'date_joined': {'N': '1366056888'},
2361                    'friend_count': {'N': '1'},
2362                    'friends': {'SS': ['johndoe']},
2363                },
2364            ],
2365            "ScannedCount": 4
2366        }
2367
2368        with mock.patch.object(
2369                self.users.connection,
2370                'query',
2371                return_value=expected) as mock_query:
2372            results = self.users._query(
2373                limit=4,
2374                reverse=True,
2375                username__between=['aaa', 'mmm']
2376            )
2377            usernames = [res['username'] for res in results['results']]
2378            self.assertEqual(usernames, ['johndoe', 'jane', 'alice', 'bob'])
2379            self.assertEqual(len(results['results']), 4)
2380            self.assertEqual(results['last_key'], None)
2381
2382        mock_query.assert_called_once_with('users',
2383            consistent_read=False,
2384            scan_index_forward=False,
2385            index_name=None,
2386            attributes_to_get=None,
2387            limit=4,
2388            key_conditions={
2389                'username': {
2390                    'AttributeValueList': [{'S': 'aaa'}, {'S': 'mmm'}],
2391                    'ComparisonOperator': 'BETWEEN',
2392                }
2393            },
2394            select=None,
2395            query_filter=None,
2396            conditional_operator=None
2397        )
2398
2399        # Now alter the expected.
2400        expected['LastEvaluatedKey'] = {
2401            'username': {
2402                'S': 'johndoe',
2403            },
2404        }
2405
2406        with mock.patch.object(
2407                self.users.connection,
2408                'query',
2409                return_value=expected) as mock_query_2:
2410            results = self.users._query(
2411                limit=4,
2412                reverse=True,
2413                username__between=['aaa', 'mmm'],
2414                exclusive_start_key={
2415                    'username': 'adam',
2416                },
2417                consistent=True,
2418                query_filter=None,
2419                conditional_operator='AND'
2420            )
2421            usernames = [res['username'] for res in results['results']]
2422            self.assertEqual(usernames, ['johndoe', 'jane', 'alice', 'bob'])
2423            self.assertEqual(len(results['results']), 4)
2424            self.assertEqual(results['last_key'], {'username': 'johndoe'})
2425
2426        mock_query_2.assert_called_once_with('users',
2427            key_conditions={
2428                'username': {
2429                    'AttributeValueList': [{'S': 'aaa'}, {'S': 'mmm'}],
2430                    'ComparisonOperator': 'BETWEEN',
2431                }
2432            },
2433            index_name=None,
2434            attributes_to_get=None,
2435            scan_index_forward=False,
2436            limit=4,
2437            exclusive_start_key={
2438                'username': {
2439                    'S': 'adam',
2440                },
2441            },
2442            consistent_read=True,
2443            select=None,
2444            query_filter=None,
2445            conditional_operator='AND'
2446        )
2447
2448    def test_private_scan(self):
2449        expected = {
2450            "ConsumedCapacity": {
2451                "CapacityUnits": 0.5,
2452                "TableName": "users"
2453            },
2454            "Count": 4,
2455            "Items": [
2456                {
2457                    'username': {'S': 'alice'},
2458                    'first_name': {'S': 'Alice'},
2459                    'last_name': {'S': 'Expert'},
2460                    'date_joined': {'N': '1366056680'},
2461                    'friend_count': {'N': '1'},
2462                    'friends': {'SS': ['jane']},
2463                },
2464                {
2465                    'username': {'S': 'bob'},
2466                    'first_name': {'S': 'Bob'},
2467                    'last_name': {'S': 'Smith'},
2468                    'date_joined': {'N': '1366056888'},
2469                    'friend_count': {'N': '1'},
2470                    'friends': {'SS': ['johndoe']},
2471                },
2472                {
2473                    'username': {'S': 'jane'},
2474                    'first_name': {'S': 'Jane'},
2475                    'last_name': {'S': 'Doe'},
2476                    'date_joined': {'N': '1366057777'},
2477                    'friend_count': {'N': '2'},
2478                    'friends': {'SS': ['alice', 'johndoe']},
2479                },
2480            ],
2481            "ScannedCount": 4
2482        }
2483
2484        with mock.patch.object(
2485                self.users.connection,
2486                'scan',
2487                return_value=expected) as mock_scan:
2488            results = self.users._scan(
2489                limit=2,
2490                friend_count__lte=2
2491            )
2492            usernames = [res['username'] for res in results['results']]
2493            self.assertEqual(usernames, ['alice', 'bob', 'jane'])
2494            self.assertEqual(len(results['results']), 3)
2495            self.assertEqual(results['last_key'], None)
2496
2497        mock_scan.assert_called_once_with('users',
2498            scan_filter={
2499                'friend_count': {
2500                    'AttributeValueList': [{'N': '2'}],
2501                    'ComparisonOperator': 'LE',
2502                }
2503            },
2504            limit=2,
2505            segment=None,
2506            attributes_to_get=None,
2507            total_segments=None,
2508            conditional_operator=None
2509        )
2510
2511        # Now alter the expected.
2512        expected['LastEvaluatedKey'] = {
2513            'username': {
2514                'S': 'jane',
2515            },
2516        }
2517
2518        with mock.patch.object(
2519                self.users.connection,
2520                'scan',
2521                return_value=expected) as mock_scan_2:
2522            results = self.users._scan(
2523                limit=3,
2524                friend_count__lte=2,
2525                exclusive_start_key={
2526                    'username': 'adam',
2527                },
2528                segment=None,
2529                total_segments=None
2530            )
2531            usernames = [res['username'] for res in results['results']]
2532            self.assertEqual(usernames, ['alice', 'bob', 'jane'])
2533            self.assertEqual(len(results['results']), 3)
2534            self.assertEqual(results['last_key'], {'username': 'jane'})
2535
2536        mock_scan_2.assert_called_once_with('users',
2537            scan_filter={
2538                'friend_count': {
2539                    'AttributeValueList': [{'N': '2'}],
2540                    'ComparisonOperator': 'LE',
2541                }
2542            },
2543            limit=3,
2544            exclusive_start_key={
2545                'username': {
2546                    'S': 'adam',
2547                },
2548            },
2549            segment=None,
2550            attributes_to_get=None,
2551            total_segments=None,
2552            conditional_operator=None
2553        )
2554
2555    def test_query(self):
2556        items_1 = {
2557            'results': [
2558                Item(self.users, data={
2559                    'username': 'johndoe',
2560                    'first_name': 'John',
2561                    'last_name': 'Doe',
2562                }),
2563                Item(self.users, data={
2564                    'username': 'jane',
2565                    'first_name': 'Jane',
2566                    'last_name': 'Doe',
2567                }),
2568            ],
2569            'last_key': 'jane',
2570        }
2571
2572        results = self.users.query_2(last_name__eq='Doe')
2573        self.assertTrue(isinstance(results, ResultSet))
2574        self.assertEqual(len(results._results), 0)
2575        self.assertEqual(results.the_callable, self.users._query)
2576
2577        with mock.patch.object(
2578                results,
2579                'the_callable',
2580                return_value=items_1) as mock_query:
2581            res_1 = next(results)
2582            # Now it should be populated.
2583            self.assertEqual(len(results._results), 2)
2584            self.assertEqual(res_1['username'], 'johndoe')
2585            res_2 = next(results)
2586            self.assertEqual(res_2['username'], 'jane')
2587
2588        self.assertEqual(mock_query.call_count, 1)
2589
2590        items_2 = {
2591            'results': [
2592                Item(self.users, data={
2593                    'username': 'foodoe',
2594                    'first_name': 'Foo',
2595                    'last_name': 'Doe',
2596                }),
2597            ],
2598        }
2599
2600        with mock.patch.object(
2601                results,
2602                'the_callable',
2603                return_value=items_2) as mock_query_2:
2604            res_3 = next(results)
2605            # New results should have been found.
2606            self.assertEqual(len(results._results), 1)
2607            self.assertEqual(res_3['username'], 'foodoe')
2608
2609            self.assertRaises(StopIteration, results.next)
2610
2611        self.assertEqual(mock_query_2.call_count, 1)
2612
2613    def test_query_with_specific_attributes(self):
2614        items_1 = {
2615            'results': [
2616                Item(self.users, data={
2617                    'username': 'johndoe',
2618                }),
2619                Item(self.users, data={
2620                    'username': 'jane',
2621                }),
2622            ],
2623            'last_key': 'jane',
2624        }
2625
2626        results = self.users.query_2(last_name__eq='Doe',
2627                                   attributes=['username'])
2628        self.assertTrue(isinstance(results, ResultSet))
2629        self.assertEqual(len(results._results), 0)
2630        self.assertEqual(results.the_callable, self.users._query)
2631
2632        with mock.patch.object(
2633                results,
2634                'the_callable',
2635                return_value=items_1) as mock_query:
2636            res_1 = next(results)
2637            # Now it should be populated.
2638            self.assertEqual(len(results._results), 2)
2639            self.assertEqual(res_1['username'], 'johndoe')
2640            self.assertEqual(list(res_1.keys()), ['username'])
2641            res_2 = next(results)
2642            self.assertEqual(res_2['username'], 'jane')
2643
2644        self.assertEqual(mock_query.call_count, 1)
2645
2646    def test_scan(self):
2647        items_1 = {
2648            'results': [
2649                Item(self.users, data={
2650                    'username': 'johndoe',
2651                    'first_name': 'John',
2652                    'last_name': 'Doe',
2653                }),
2654                Item(self.users, data={
2655                    'username': 'jane',
2656                    'first_name': 'Jane',
2657                    'last_name': 'Doe',
2658                }),
2659            ],
2660            'last_key': 'jane',
2661        }
2662
2663        results = self.users.scan(last_name__eq='Doe')
2664        self.assertTrue(isinstance(results, ResultSet))
2665        self.assertEqual(len(results._results), 0)
2666        self.assertEqual(results.the_callable, self.users._scan)
2667
2668        with mock.patch.object(
2669                results,
2670                'the_callable',
2671                return_value=items_1) as mock_scan:
2672            res_1 = next(results)
2673            # Now it should be populated.
2674            self.assertEqual(len(results._results), 2)
2675            self.assertEqual(res_1['username'], 'johndoe')
2676            res_2 = next(results)
2677            self.assertEqual(res_2['username'], 'jane')
2678
2679        self.assertEqual(mock_scan.call_count, 1)
2680
2681        items_2 = {
2682            'results': [
2683                Item(self.users, data={
2684                    'username': 'zoeydoe',
2685                    'first_name': 'Zoey',
2686                    'last_name': 'Doe',
2687                }),
2688            ],
2689        }
2690
2691        with mock.patch.object(
2692                results,
2693                'the_callable',
2694                return_value=items_2) as mock_scan_2:
2695            res_3 = next(results)
2696            # New results should have been found.
2697            self.assertEqual(len(results._results), 1)
2698            self.assertEqual(res_3['username'], 'zoeydoe')
2699
2700            self.assertRaises(StopIteration, results.next)
2701
2702        self.assertEqual(mock_scan_2.call_count, 1)
2703
2704    def test_scan_with_specific_attributes(self):
2705        items_1 = {
2706            'results': [
2707                Item(self.users, data={
2708                    'username': 'johndoe',
2709                }),
2710                Item(self.users, data={
2711                    'username': 'jane',
2712                }),
2713            ],
2714            'last_key': 'jane',
2715        }
2716
2717        results = self.users.scan(attributes=['username'])
2718        self.assertTrue(isinstance(results, ResultSet))
2719        self.assertEqual(len(results._results), 0)
2720        self.assertEqual(results.the_callable, self.users._scan)
2721
2722        with mock.patch.object(
2723                results,
2724                'the_callable',
2725                return_value=items_1) as mock_query:
2726            res_1 = next(results)
2727            # Now it should be populated.
2728            self.assertEqual(len(results._results), 2)
2729            self.assertEqual(res_1['username'], 'johndoe')
2730            self.assertEqual(list(res_1.keys()), ['username'])
2731            res_2 = next(results)
2732            self.assertEqual(res_2['username'], 'jane')
2733
2734        self.assertEqual(mock_query.call_count, 1)
2735
2736    def test_count(self):
2737        expected = {
2738            "Table": {
2739                "AttributeDefinitions": [
2740                    {
2741                        "AttributeName": "username",
2742                        "AttributeType": "S"
2743                    }
2744                ],
2745                "ItemCount": 5,
2746                "KeySchema": [
2747                    {
2748                        "AttributeName": "username",
2749                        "KeyType": "HASH"
2750                    }
2751                ],
2752                "LocalSecondaryIndexes": [
2753                    {
2754                        "IndexName": "UsernameIndex",
2755                        "KeySchema": [
2756                            {
2757                                "AttributeName": "username",
2758                                "KeyType": "HASH"
2759                            }
2760                        ],
2761                        "Projection": {
2762                            "ProjectionType": "KEYS_ONLY"
2763                        }
2764                    }
2765                ],
2766                "ProvisionedThroughput": {
2767                    "ReadCapacityUnits": 20,
2768                    "WriteCapacityUnits": 6
2769                },
2770                "TableName": "Thread",
2771                "TableStatus": "ACTIVE"
2772            }
2773        }
2774
2775        with mock.patch.object(
2776                self.users,
2777                'describe',
2778                return_value=expected) as mock_count:
2779            self.assertEqual(self.users.count(), 5)
2780
2781    def test_query_count_simple(self):
2782        expected_0 = {
2783            'Count': 0.0,
2784        }
2785
2786        expected_1 = {
2787            'Count': 10.0,
2788        }
2789
2790        with mock.patch.object(
2791                self.users.connection,
2792                'query',
2793                return_value=expected_0) as mock_query:
2794            results = self.users.query_count(username__eq='notmyname')
2795            self.assertTrue(isinstance(results, int))
2796            self.assertEqual(results, 0)
2797        self.assertEqual(mock_query.call_count, 1)
2798        self.assertIn('scan_index_forward', mock_query.call_args[1])
2799        self.assertEqual(True, mock_query.call_args[1]['scan_index_forward'])
2800        self.assertIn('limit', mock_query.call_args[1])
2801        self.assertEqual(None, mock_query.call_args[1]['limit'])
2802
2803        with mock.patch.object(
2804                self.users.connection,
2805                'query',
2806                return_value=expected_1) as mock_query:
2807            results = self.users.query_count(username__gt='somename', consistent=True, scan_index_forward=False, limit=10)
2808            self.assertTrue(isinstance(results, int))
2809            self.assertEqual(results, 10)
2810        self.assertEqual(mock_query.call_count, 1)
2811        self.assertIn('scan_index_forward', mock_query.call_args[1])
2812        self.assertEqual(False, mock_query.call_args[1]['scan_index_forward'])
2813        self.assertIn('limit', mock_query.call_args[1])
2814        self.assertEqual(10, mock_query.call_args[1]['limit'])
2815
2816    def test_query_count_paginated(self):
2817        def return_side_effect(*args, **kwargs):
2818            if kwargs.get('exclusive_start_key'):
2819                return {'Count': 10, 'LastEvaluatedKey': None}
2820            else:
2821                return {
2822                    'Count': 20,
2823                    'LastEvaluatedKey': {
2824                        'username': {
2825                            'S': 'johndoe'
2826                        },
2827                        'date_joined': {
2828                            'N': '4118642633'
2829                        }
2830                    }
2831                }
2832
2833        with mock.patch.object(
2834                self.users.connection,
2835                'query',
2836                side_effect=return_side_effect
2837        ) as mock_query:
2838            count = self.users.query_count(username__eq='johndoe')
2839            self.assertTrue(isinstance(count, int))
2840            self.assertEqual(30, count)
2841            self.assertEqual(mock_query.call_count, 2)
2842
2843    def test_private_batch_get(self):
2844        expected = {
2845            "ConsumedCapacity": {
2846                "CapacityUnits": 0.5,
2847                "TableName": "users"
2848            },
2849            'Responses': {
2850                'users': [
2851                    {
2852                        'username': {'S': 'alice'},
2853                        'first_name': {'S': 'Alice'},
2854                        'last_name': {'S': 'Expert'},
2855                        'date_joined': {'N': '1366056680'},
2856                        'friend_count': {'N': '1'},
2857                        'friends': {'SS': ['jane']},
2858                    },
2859                    {
2860                        'username': {'S': 'bob'},
2861                        'first_name': {'S': 'Bob'},
2862                        'last_name': {'S': 'Smith'},
2863                        'date_joined': {'N': '1366056888'},
2864                        'friend_count': {'N': '1'},
2865                        'friends': {'SS': ['johndoe']},
2866                    },
2867                    {
2868                        'username': {'S': 'jane'},
2869                        'first_name': {'S': 'Jane'},
2870                        'last_name': {'S': 'Doe'},
2871                        'date_joined': {'N': '1366057777'},
2872                        'friend_count': {'N': '2'},
2873                        'friends': {'SS': ['alice', 'johndoe']},
2874                    },
2875                ],
2876            },
2877            "UnprocessedKeys": {
2878            },
2879        }
2880
2881        with mock.patch.object(
2882                self.users.connection,
2883                'batch_get_item',
2884                return_value=expected) as mock_batch_get:
2885            results = self.users._batch_get(keys=[
2886                {'username': 'alice', 'friend_count': 1},
2887                {'username': 'bob', 'friend_count': 1},
2888                {'username': 'jane'},
2889            ])
2890            usernames = [res['username'] for res in results['results']]
2891            self.assertEqual(usernames, ['alice', 'bob', 'jane'])
2892            self.assertEqual(len(results['results']), 3)
2893            self.assertEqual(results['last_key'], None)
2894            self.assertEqual(results['unprocessed_keys'], [])
2895
2896        mock_batch_get.assert_called_once_with(request_items={
2897            'users': {
2898                'Keys': [
2899                    {
2900                        'username': {'S': 'alice'},
2901                        'friend_count': {'N': '1'}
2902                    },
2903                    {
2904                        'username': {'S': 'bob'},
2905                        'friend_count': {'N': '1'}
2906                    }, {
2907                        'username': {'S': 'jane'},
2908                    }
2909                ]
2910            }
2911        })
2912
2913        # Now alter the expected.
2914        del expected['Responses']['users'][2]
2915        expected['UnprocessedKeys'] = {
2916            'users': {
2917                'Keys': [
2918                    {'username': {'S': 'jane',}},
2919                ],
2920            },
2921        }
2922
2923        with mock.patch.object(
2924                self.users.connection,
2925                'batch_get_item',
2926                return_value=expected) as mock_batch_get_2:
2927            results = self.users._batch_get(keys=[
2928                {'username': 'alice', 'friend_count': 1},
2929                {'username': 'bob', 'friend_count': 1},
2930                {'username': 'jane'},
2931            ])
2932            usernames = [res['username'] for res in results['results']]
2933            self.assertEqual(usernames, ['alice', 'bob'])
2934            self.assertEqual(len(results['results']), 2)
2935            self.assertEqual(results['last_key'], None)
2936            self.assertEqual(results['unprocessed_keys'], [
2937                {'username': 'jane'}
2938            ])
2939
2940        mock_batch_get_2.assert_called_once_with(request_items={
2941            'users': {
2942                'Keys': [
2943                    {
2944                        'username': {'S': 'alice'},
2945                        'friend_count': {'N': '1'}
2946                    },
2947                    {
2948                        'username': {'S': 'bob'},
2949                        'friend_count': {'N': '1'}
2950                    }, {
2951                        'username': {'S': 'jane'},
2952                    }
2953                ]
2954            }
2955        })
2956
2957    def test_private_batch_get_attributes(self):
2958        # test if AttributesToGet parameter is passed to DynamoDB API
2959        expected = {
2960            "ConsumedCapacity": {
2961                "CapacityUnits": 0.5,
2962                "TableName": "users"
2963            },
2964            'Responses': {
2965                'users': [
2966                    {
2967                        'username': {'S': 'alice'},
2968                        'first_name': {'S': 'Alice'},
2969                    },
2970                    {
2971                        'username': {'S': 'bob'},
2972                        'first_name': {'S': 'Bob'},
2973                    },
2974                ],
2975            },
2976            "UnprocessedKeys": {},
2977        }
2978
2979        with mock.patch.object(
2980                self.users.connection,
2981                'batch_get_item',
2982                return_value=expected) as mock_batch_get_attr:
2983            results = self.users._batch_get(keys=[
2984                    {'username': 'alice'},
2985                    {'username': 'bob'},
2986                ], attributes=['username', 'first_name'])
2987            usernames = [res['username'] for res in results['results']]
2988            first_names = [res['first_name'] for res in results['results']]
2989            self.assertEqual(usernames, ['alice', 'bob'])
2990            self.assertEqual(first_names, ['Alice', 'Bob'])
2991            self.assertEqual(len(results['results']), 2)
2992            self.assertEqual(results['last_key'], None)
2993            self.assertEqual(results['unprocessed_keys'], [])
2994
2995        mock_batch_get_attr.assert_called_once_with(request_items={
2996            'users': {
2997                'Keys': [ { 'username': {'S': 'alice'} },
2998                          { 'username': {'S': 'bob'} }, ],
2999                'AttributesToGet': ['username', 'first_name'],
3000            },
3001        })
3002
3003    def test_batch_get(self):
3004        items_1 = {
3005            'results': [
3006                Item(self.users, data={
3007                    'username': 'johndoe',
3008                    'first_name': 'John',
3009                    'last_name': 'Doe',
3010                }),
3011                Item(self.users, data={
3012                    'username': 'jane',
3013                    'first_name': 'Jane',
3014                    'last_name': 'Doe',
3015                }),
3016            ],
3017            'last_key': None,
3018            'unprocessed_keys': [
3019                'zoeydoe',
3020            ]
3021        }
3022
3023        results = self.users.batch_get(keys=[
3024            {'username': 'johndoe'},
3025            {'username': 'jane'},
3026            {'username': 'zoeydoe'},
3027        ])
3028        self.assertTrue(isinstance(results, BatchGetResultSet))
3029        self.assertEqual(len(results._results), 0)
3030        self.assertEqual(results.the_callable, self.users._batch_get)
3031
3032        with mock.patch.object(
3033                results,
3034                'the_callable',
3035                return_value=items_1) as mock_batch_get:
3036            res_1 = next(results)
3037            # Now it should be populated.
3038            self.assertEqual(len(results._results), 2)
3039            self.assertEqual(res_1['username'], 'johndoe')
3040            res_2 = next(results)
3041            self.assertEqual(res_2['username'], 'jane')
3042
3043        self.assertEqual(mock_batch_get.call_count, 1)
3044        self.assertEqual(results._keys_left, ['zoeydoe'])
3045
3046        items_2 = {
3047            'results': [
3048                Item(self.users, data={
3049                    'username': 'zoeydoe',
3050                    'first_name': 'Zoey',
3051                    'last_name': 'Doe',
3052                }),
3053            ],
3054        }
3055
3056        with mock.patch.object(
3057                results,
3058                'the_callable',
3059                return_value=items_2) as mock_batch_get_2:
3060            res_3 = next(results)
3061            # New results should have been found.
3062            self.assertEqual(len(results._results), 1)
3063            self.assertEqual(res_3['username'], 'zoeydoe')
3064
3065            self.assertRaises(StopIteration, results.next)
3066
3067        self.assertEqual(mock_batch_get_2.call_count, 1)
3068        self.assertEqual(results._keys_left, [])