def test_dynamodb_delete_all_items(): table_name = "awsimple-delete-test" # this test is the only thing we'll use this table for dynamodb_access = DynamoDBAccess(table_name, profile_name=test_awsimple_str) dynamodb_access.create_table(id_str) dynamodb_access.put_item(dict_to_dynamodb({id_str: "me", "answer": 42})) dynamodb_access.put_item(dict_to_dynamodb({id_str: "you", "question": 0})) while len(table_contents := dynamodb_access.scan_table()) != 2: print(f"waiting for the put ...{table_contents}") time.sleep(1) # DynamoDB is "eventually consistent"
def test_dynamodb_delete(): dynamodb_access = DynamoDBAccess(profile_name=test_awsimple_str, table_name=test_awsimple_str) dynamodb_access.create_table(id_str) test_id = "deleter" item_value = {id_str: test_id, "color": "blue"} dynamodb_access.put_item(item_value) assert dynamodb_access.get_item( id_str, test_id) == item_value # check that it's set dynamodb_access.delete_item(id_str, test_id) with pytest.raises(DBItemNotFound): print(dynamodb_access.get_item(id_str, test_id)) # check that it's deleted
def test_dynamodb(): dynamodb_dict = dict_to_dynamodb(sample_input) assert dynamodb_dict["sample1"] == "Test Data" assert math.isclose(float(dynamodb_dict["sample2"]), decimal.Decimal(2.0)) assert dynamodb_dict["sample3"] is True assert dynamodb_dict["sample5"] is None assert dynamodb_dict["sample6"] == {"test": True} assert dynamodb_dict["sample7"] == ["Hello", "World"] assert dynamodb_dict["sample8"] == [ decimal.Decimal(9), decimal.Decimal(10) ] assert dynamodb_dict["DecimalInt"] == decimal.Decimal(42) assert dynamodb_dict["DecimalFloat"] == decimal.Decimal( 2.0) / decimal.Decimal(3.0) assert dynamodb_dict["a_tuple"] == [1, 2, 3] assert dynamodb_dict[ "42"] == "my_key_is_an_int" # test conversion of an int key to a string assert dynamodb_dict["test_date_time"] == "2019-06-04T20:18:55+00:00" assert dynamodb_dict["zero_len_string"] is None # while dictim is case insensitive, when we convert to dict for DynamoDB it becomes case sensitive assert list(dynamodb_dict["dictim"]["HI"])[0] == "there" assert dynamodb_dict["dictim"]["HI"]["there"] == 1 # actually Decimal(1) assert dynamodb_dict["dictim"].get( "hi") is None # we're back to case sensitivity dynamodb_access = DynamoDBAccess( profile_name=test_awsimple_str, table_name=test_awsimple_str, cache_dir=Path("cache"), cache_life=timedelta(seconds=1).total_seconds()) dynamodb_access.create_table(id_str) dynamodb_access.put_item(dynamodb_dict) sample_from_db = dynamodb_access.get_item(id_str, dict_id) assert sample_from_db == dynamodb_dict # make sure we get back exactly what we wrote table_contents = dynamodb_access.scan_table_cached() assert not dynamodb_access.cache_hit check_table_contents(table_contents) table_contents = dynamodb_access.scan_table() check_table_contents(table_contents) table_contents = dynamodb_access.scan_table_cached() assert dynamodb_access.cache_hit check_table_contents(table_contents) assert dynamodb_access.get_primary_keys() == (id_str, None) # no sort key
def test_dynamodb_sort_as_number(): dynamodb_access = DynamoDBAccess( profile_name=test_awsimple_str, table_name=f"{test_awsimple_str}_sort_as_number", cache_dir=Path("cache")) dynamodb_access.create_table("id", "year", sort_key_type=int) # sort key as number input_item = {"id": "me", "year": 1999, "out_of_time": False} dynamodb_access.put_item(input_item) item = dynamodb_access.get_item("id", "me", "year", 1999) output_item = dynamodb_to_dict(item) pprint(item) assert input_item == output_item dynamodb_access.delete_table()
def test_dynamodb_secondary_index(): table_name = f"{test_awsimple_str}2" table = DynamoDBAccess(table_name) sort_key = "id2" secondary_index = "id3" table.create_table(id_str, sort_key, secondary_index) item = {id_str: "me", sort_key: "myself", secondary_index: "i"} table.put_item(item) item2 = deepcopy(item) item2[sort_key] = "moi même" # also test unicode! item2[secondary_index] = "je" table.put_item(item2) query_results = table.query(id_str, "me") print(f"{query_results=}") assert len( query_results ) == 2 # just the partition key should provide us with both rows assert table.query(secondary_index, "je") == [ item2 ] # with (only) the secondary index (in DynamoDB you can't mix primary and secondary indexes) expected_contents = { DictKey(partition="me", sort="moi même"): { "id": "me", "id2": "moi même", "id3": "je" }, DictKey(partition="me", sort="myself"): { "id": "me", "id2": "myself", "id3": "i" }, } contents = table.scan_table_cached_as_dict() assert contents == expected_contents assert list(contents.keys()) == [ DictKey(partition="me", sort="moi même"), DictKey(partition="me", sort="myself") ] table.delete_table()
def test_dynamodb_partition_as_number(): dynamodb_access = DynamoDBAccess( profile_name=test_awsimple_str, table_name=f"{test_awsimple_str}_partition_as_number", cache_dir=Path("cache")) dynamodb_access.create_table( "year", "id", partition_key_type=int) # partition key as number input_item = {"id": "me", "year": 1999, "out_of_time": False} dynamodb_access.put_item(input_item) item = dynamodb_access.get_item("id", "me", "year", 1999) pprint(item) assert input_item == dynamodb_to_dict(item) item = dynamodb_access.query( "year", 1999)[0] # only use the partition key (no sort key) pprint(item) assert input_item == dynamodb_to_dict(item) dynamodb_access.delete_table()
def test_dynamodb_secondary_index_int(): table_name = f"{test_awsimple_str}3" table = DynamoDBAccess(table_name) sort_key = "id2" secondary_index = "num" table.create_table(id_str, sort_key, secondary_index, secondary_key_type=int) # secondary index as an int table.put_item({id_str: "me", sort_key: "myself", secondary_index: 1}) table.put_item({id_str: "me", sort_key: "moi", secondary_index: 2}) query_results = table.query(id_str, "me") print(f"{query_results=}") assert len( query_results ) == 2 # just the partition key should provide us with both rows table.delete_table()
def test_dynamodb_scan_table_as_dict(): dynamodb_access = DynamoDBAccess(profile_name=test_awsimple_str, table_name=test_awsimple_str, cache_dir=Path("cache"), cache_life=timedelta(seconds=10).total_seconds()) dynamodb_access.create_table(id_str) dynamodb_access.put_item({id_str: "b", "value": 1}) # will be sorted in a different order than we're inputting dynamodb_access.put_item({id_str: "c", "value": 3}) dynamodb_access.put_item({id_str: "a", "value": 2}) expected_contents = {"a": {"id": "a", "value": Decimal("2")}, "b": {"id": "b", "value": Decimal("1")}, "c": {"id": "c", "value": Decimal("3")}} table_contents = dynamodb_access.scan_table_as_dict() check_scan_table(table_contents, expected_contents) table_contents = dynamodb_access.scan_table_cached_as_dict() check_scan_table(table_contents, expected_contents) table_contents = dynamodb_access.scan_table_cached_as_dict() assert dynamodb_access.cache_hit check_scan_table(table_contents, expected_contents) table_contents = dynamodb_access.scan_table_cached_as_dict(sort_key=lambda x: x[id_str]) # test sort_key check_scan_table(table_contents, expected_contents)
def test_dynamodb_query(): table_name = "testawsimpleps" # ps = both partition and sort dynamodb_access = DynamoDBAccess(profile_name=test_awsimple_str, table_name=table_name) dynamodb_access.create_table("id", "name") # three entries for "me" dynamodb_access.put_item({ "id": "me", "name": "james", "answer": 13 }) # this will be the "first" one dynamodb_access.put_item({"id": "me", "name": "james abel", "answer": 1}) dynamodb_access.put_item({ "id": "me", "name": "zzz", "answer": 99 }) # this will be the "last" one dynamodb_access.put_item({"id": "notme", "name": "notjames", "answer": 42}) response = dynamodb_access.query("id", "me") # partition only assert len(response) == 3 response = dynamodb_access.query("id", "me", "name", "james") # partition and sort assert len(response) == 1 response = dynamodb_access.query_begins_with("id", "me", "name", "james a") # begins with assert len(response) == 1 response = dynamodb_access.query_begins_with("id", "me", "name", "jame") assert len(response) == 2 response = dynamodb_access.query("id", "idonotexist") # does not exist assert len(response) == 0 response = dynamodb_access.query_one("id", "me", QuerySelection.highest) assert response["answer"] == 99 assert response["name"] == "zzz" # the "last" entry, as sorted by sort key response = dynamodb_access.query_one("id", "me", QuerySelection.lowest) assert response["answer"] == 13 assert response[ "name"] == "james" # the "first" entry, as sorted by sort key response = dynamodb_access.query_one("id", "idonotexist", QuerySelection.lowest) assert response is None
def test_dynamodb_create_table(): table_name = f"{test_awsimple_str}temp" dynamodb_access = DynamoDBAccess(table_name, profile_name=test_awsimple_str) dynamodb_access.create_table("id") assert dynamodb_access.table_exists( ) # create_table has a waiter so the table should exist at this point dynamodb_access.put_item({"id": "me", "value": 1}) table_data = dynamodb_access.scan_table_cached() pprint(table_data) assert table_data[0]["id"] == "me" assert table_data[0]["value"] == 1 assert len(table_data) == 1 assert len(dynamodb_access.scan_table_cached(invalidate_cache=True)) == 1 dynamodb_access.delete_table() assert not dynamodb_access.delete_table( ) # delete_table has a waiter so the table should exist at this point
def users_example(): """ This example shows how to use DynamoDB to keep a table of users. This also illustrates the flexibility of NoSQL in that we can simply add fields at any time. """ dynamodb_access = DynamoDBAccess("users_example", profile_name="testawsimple") # we're only using email as a partition key in our primary key (no sort key). emails are unique to each user. dynamodb_access.create_table("email") # add our first user using email, first and last name. Initially, we may think that's all we need. dynamodb_access.put_item({ "email": "*****@*****.**", "first_name": "Victor", "last_name": "Wooten" }) # oh no. No one knows who "John Jones" is, they only know "John Paul Jones", so we need to add a middle name. # Luckily we are using a NoSQL database, so we just add "middle_name" in a new key/value pair. No database migration needed. dynamodb_access.put_item({ "email": "*****@*****.**", "first_name": "John", "middle_name": "Paul", "last_name": "Jones" }) # oh no again. No one knows who "Gordon Matthew Thomas Sumner" is either, even with 2 middle names! All they know is "Sting". # We need to add a nickname. No problem since we're using a NoSQL database. dynamodb_access.put_item({ "email": "*****@*****.**", "first_name": "Gordon", "middle_name": "Matthew", "middle_name_2": "Thomas", "last_name": "Sumner", "nickname": "Sting", }) # look up user info for one of our users start = time.time() item = dynamodb_access.get_item( "email", "*****@*****.**" ) # this is a "get" since we're using a key and will always get back exactly one item end = time.time() pprint(item) print( f"took {end-start} seconds" ) # should take just a fraction of a second. 0.05 seconds was a nominal value on our test system.
def musical_instruments_example(): """ This example shows how to use DynamoDB to keep a table of musical instruments. """ dynamodb_access = DynamoDBAccess("musical_instruments_example", profile_name="testawsimple", cache_life=60) # short cache life for development # Our primary key is a composite of partition (manufacturer) and sort (serial_number). # For a particular manufacturer, serial numbers define exactly one instrument (for this example we are assuming a serial number can be represented as an # integer and doesn't have to be a string). dynamodb_access.create_table("manufacturer", "serial_number", sort_key_type=int) # we have to convert float to a Decimal for DynamoDB dynamodb_access.put_item(dict_to_dynamodb({"manufacturer": "Gibson", "serial_number": 1234, "model": "Ripper", "year": 1983, "price": 1299.50})) dynamodb_access.put_item(dict_to_dynamodb({"manufacturer": "Gibson", "serial_number": 5678, "model": "Thunderbird", "year": 1977, "price": 2399.50})) dynamodb_access.put_item( dict_to_dynamodb( { "manufacturer": "Fender", "serial_number": 1234, "model": "Precision", "year": 2008, "price": 1800.0, } # same serial number as the Gibson Ripper, but that's OK since this is Fender ) ) # get all the Gibson instruments start = time.time() item = dynamodb_access.query("manufacturer", "Gibson") # this can (and will in this case) be multiple items end = time.time() pprint(item) print(f"query took {end-start} seconds") # nominal 0.1 to 0.15 seconds print() # get the entire inventory start = time.time() all_items = dynamodb_access.scan_table_cached() # use cached if the table is large and *only* if we know our table is slowly or never changing end = time.time() pprint(all_items) print(f"scan took {end-start} seconds ({dynamodb_access.cache_hit=})") # always fast for this small data set, but caching can offer a speedup for large tables