def __init__(self, dynamodb_table: str, table_schema: Union[Dict[str, type], Dict[str, str]]): super().__init__(dynamodb_table=dynamodb_table) self.validation = Validation(table_schema=table_schema) self.client_exceptions: tuple(Exception) = ( dynamodb_exceptions.DynamoDbWrongKeyError, dynamodb_exceptions.ValidationWrongSchemaTypeError, dynamodb_exceptions.ValidationWrongKeyError, dynamodb_exceptions.ValidationMissingKeyError, dynamodb_exceptions.ValidationIncorrectKeyTypeError, dynamodb_exceptions.ValidationIncorrectAttributeError, dynamodb_exceptions.ValidationFailedAttributesUpdateError, dynamodb_exceptions.DynamoDbInvalidTableError, dynamodb_exceptions.DynamoDbWrongKeyFormatError, )
def test_validation_schema_delete_item(self): validation: Validation = Validation( table_schema=self.generate_schema_template()) self.assertEqual( validation.validation_schema(validation_type="delete_item"), validation.dynamodb_key_schema, )
def test_validation_schema_updated_item(self): validation: Validation = Validation( table_schema=self.generate_schema_template()) self.assertEqual( validation.validation_schema(validation_type="update_item"), validation.update_item_schema, )
def test_generate_expression_attribute_values_with_wrong_key(self): dynamodb_api: DynamodbApi = DynamodbApi(dynamodb_table="test_table") validation: Validation = Validation( table_schema=self.generate_schema_template()) self.assertRaises(ValidationIncorrectAttributeError, dynamodb_api.generate_expression_attribute_values, new_attributes={ "address": "Rochet trust drive", "age": "35", "comic": "Batman", }, dynamodb_validation_format_mapper={ "CustomerId": { "dynamodb_type": "S" }, "name": { "dynamodb_type": "S" }, "address": { "dynamodb_type": "S" }, "age": { "dynamodb_type": "S" }, "car": { "dynamodb_type": "S" }, }, expression_mapping=validation.expression_mapping)
def test_validate_item_to_readable_format(self): validation: Validation = Validation( table_schema=self.generate_schema_template()) self.assertEqual( validation.validate_item_to_readable_format( dynamodb_item={ "CustomerId": { "S": "1482328791" }, "name": { "S": "James Joseph" }, "address": { "S": "Jeff Bezos Candy land road" }, "age": { "S": "32" }, "car": { "S": "Black Skoda" }, }), { "CustomerId": "1482328791", "name": "James Joseph", "address": "Jeff Bezos Candy land road", "age": "32", "car": "Black Skoda", }, )
def test_validate_attributes_not_updated(self): validation: Validation = Validation( table_schema=self.generate_schema_template()) self.assertRaises( ValidationIncorrectAttributesError, validation.validate_attributes_updated, response={ "Attributes": { "age": { "S": "32" }, "car": { "S": "Black Skoda" } } }, validated_new_attributes={ "age": { "S": "35" }, "car": { "S": "Blue BMW" } }, )
def test_expression_expression_mapper(self): validation: Validation = Validation( table_schema=self.generate_schema_template()) validation.generate_expression_mapper() self.assertEqual( validation.expression_mapping, { "name": { "expression_attribute_name": "#NA", "expression_attribute_var": ":na", }, "address": { "expression_attribute_name": "#AD", "expression_attribute_var": ":ad", }, "age": { "expression_attribute_name": "#A", "expression_attribute_var": ":a", }, "car": { "expression_attribute_name": "#CA", "expression_attribute_var": ":ca", }, "CustomerId": { "expression_attribute_name": "#C", "expression_attribute_var": ":c", }, }, )
def test_failed_to_grab_validation_Schema(self): validation: Validation = Validation( table_schema=self.generate_schema_template()) self.assertRaises( ValidationWrongSchemaTypeError, validation.validation_schema, validation_type="not a schema", )
def test_generate_key_schema(self): validation = Validation(table_schema=self.generate_schema_template()) self.assertEqual( validation.dynamodb_key_schema.json_schema("CustomerId"), Schema({ "CustomerId": And(Use(str)) }).json_schema("CustomerId"), )
def test_expression_selection(self): validation: Validation = Validation( table_schema=self.generate_schema_template()) self.assertEqual( validation.expression_selection(attribute="CustomerId"), { "expression_attribute_name": "#U", "expression_attribute_var": ":u" }, )
def test_bad_format_schema(self): with self.assertRaises(ValidationWrongKeyError): Validation( table_schema={ "CustomerId": str, "name": str, "address": str, "age": str, "car": str, })
def test_validate_item_data_entegrity_delete_item(self): validation: Validation = Validation( table_schema=self.generate_schema_template()) self.assertEqual( validation.validate_item_data_entegrity( dynamodb_schema=validation.dynamodb_key_schema, unvalidated_item={"CustomerId": "1482328791"}, ), {"CustomerId": "1482328791"}, )
def test_generate_update_schema(self): validation = Validation(table_schema=self.generate_schema_template()) self.assertEqual( validation.update_item_schema.json_schema("CustomerId"), Schema({ Optional("name"): And(Use(str)), Optional("address"): And(Use(str)), Optional("age"): And(Use(str)), Optional("car"): And(Use(str)), "CustomerId": (And(Use(str))), }).json_schema("CustomerId"), )
def test_generate_update_expression_with_wrong_key(self): dynamodb_api: DynamodbApi = DynamodbApi(dynamodb_table="test_table") validation: Validation = Validation( table_schema=self.generate_schema_template()) self.assertRaises(ValidationIncorrectAttributeError, dynamodb_api.generate_update_expression, new_attributes={ "address": "Rochet trust drive", "age": "35", "comic": "Batman", }, expression_mapping=validation.expression_mapping)
def test_generate_new_item_schema(self): validation = Validation(table_schema=self.generate_schema_template()) self.assertEqual( validation.new_item_schema.json_schema("CustomerId"), Schema({ "name": And(Use(str)), "address": And(Use(str)), "age": And(Use(str)), "car": And(Use(str)), "CustomerId": (And(Use(str))), }).json_schema("CustomerId"), )
def test_generate_update_expression(self): dynamodb_api: DynamodbApi = DynamodbApi(dynamodb_table="test_table") validation: Validation = Validation( table_schema=self.generate_schema_template()) self.assertEqual( dynamodb_api.generate_update_expression( new_attributes={ "address": "Rochet trust drive", "age": "35" }, expression_mapping=validation.expression_mapping), "SET #A = :a, #AG = :ag", )
def test_validate_new_attributes_exist(self): validation: Validation = Validation( table_schema=self.generate_schema_template()) self.assertEqual( validation.validate_new_attributes_exist( item_attributes={ "name": "James Joseph", "address": "Jeff Bezos Candy land road", }), { "name": "James Joseph", "address": "Jeff Bezos Candy land road" }, )
def test_validate_item_data_entegrity_wrong_key(self): validation: Validation = Validation( table_schema=self.generate_schema_template()) self.assertRaises( ValidationWrongKeyError, validation.validate_item_data_entegrity, dynamodb_schema=validation.update_item_schema, unvalidated_item={ "CustomerId": "1482328790", "name": "James Joseph", "address": "Jeff Bezos Candy land road", "comics": "Batman", }, )
def test_validate_attributes_failed_to_update(self): validation: Validation = Validation( table_schema=self.generate_schema_template()) self.assertRaises( ValidationFailedAttributesUpdateError, validation.validate_attributes_updated, response={}, validated_new_attributes={ "age": { "S": "32" }, "car": { "S": "Black Skoda" } }, )
def test_format_schema(self): validation = Validation(table_schema=self.generate_schema_template()) self.assertEqual( (validation.schema_template, validation.key_template), ( { "CustomerId": str, "name": str, "address": str, "age": str, "car": str, }, { "CustomerId": str }, ), )
def test_validate_item_data_entegrity_update_item(self): validation: Validation = Validation( table_schema=self.generate_schema_template()) self.assertEqual( validation.validate_item_data_entegrity( dynamodb_schema=validation.update_item_schema, unvalidated_item={ "CustomerId": "1482328791", "name": "James Joseph", "address": "Jeff Bezos Candy land road", }, ), { "CustomerId": "1482328791", "name": "James Joseph", "address": "Jeff Bezos Candy land road", }, )
def test_generate_format_mapper(self): validation = Validation(table_schema=self.generate_schema_template()) self.assertEqual( validation.dynamodb_format_mapper, { "name": { "dynamodb_type": "S" }, "address": { "dynamodb_type": "S" }, "age": { "dynamodb_type": "S" }, "car": { "dynamodb_type": "S" }, "CustomerId": { "dynamodb_type": "S" }, }, )
def test_validate_item_data_entegrity_int_to_string(self): validation: Validation = Validation( table_schema=self.generate_schema_template()) self.assertEqual( validation.validate_item_data_entegrity( dynamodb_schema=validation.new_item_schema, unvalidated_item={ "CustomerId": 1482328791, "name": "James Joseph", "address": "Jeff Bezos Candy land road", "age": 32, "car": "Black Skoda", }, ), { "CustomerId": "1482328791", "name": "James Joseph", "address": "Jeff Bezos Candy land road", "age": "32", "car": "Black Skoda", }, )
def test_generate_expression_attribute_values(self): dynamodb_api: DynamodbApi = DynamodbApi(dynamodb_table="test_table") validation: Validation = Validation( table_schema=self.generate_schema_template()) self.assertEqual( dynamodb_api.generate_expression_attribute_values( new_attributes={ "address": "Rochet trust drive", "age": "35" }, dynamodb_validation_format_mapper={ "CustomerId": { "dynamodb_type": "S" }, "name": { "dynamodb_type": "S" }, "address": { "dynamodb_type": "S" }, "age": { "dynamodb_type": "S" }, "car": { "dynamodb_type": "S" }, }, expression_mapping=validation.expression_mapping), { ":a": { "S": "Rochet trust drive" }, ":ag": { "S": "35" } }, )
def test_validate_attributes_updated(self): validation: Validation = Validation( table_schema=self.generate_schema_template()) self.assertTrue( validation.validate_attributes_updated( response={ "Attributes": { "age": { "S": "32" }, "car": { "S": "Black Skoda" } } }, validated_new_attributes={ "age": { "S": "32" }, "car": { "S": "Black Skoda" }, }, ))
class DynamodbClient(DynamodbApi): def __init__(self, dynamodb_table: str, table_schema: Union[Dict[str, type], Dict[str, str]]): super().__init__(dynamodb_table=dynamodb_table) self.validation = Validation(table_schema=table_schema) self.client_exceptions: tuple(Exception) = ( dynamodb_exceptions.DynamoDbWrongKeyError, dynamodb_exceptions.ValidationWrongSchemaTypeError, dynamodb_exceptions.ValidationWrongKeyError, dynamodb_exceptions.ValidationMissingKeyError, dynamodb_exceptions.ValidationIncorrectKeyTypeError, dynamodb_exceptions.ValidationIncorrectAttributeError, dynamodb_exceptions.ValidationFailedAttributesUpdateError, dynamodb_exceptions.DynamoDbInvalidTableError, dynamodb_exceptions.DynamoDbWrongKeyFormatError, ) def validate_data(self, validation_type: str, unvalidated_data: Dict[str, str]) -> Dict[str, str]: """Designed to validate the data using the validation_schema in the validation module Args: validation_type (str): Choose between ['new_item', 'update_item', 'delete_item', 'read_item'] unvalidated_data (Dict[str, str]): The attributes for the item you want to validate are correct Returns: Dict[str, str]: Validated data with the correct format """ validation_schema: Schema = self.validation.validation_schema( validation_type=validation_type) return self.validation.validate_item_data_entegrity( dynamodb_schema=validation_schema, unvalidated_item=unvalidated_data) def create_item(self, dynamodb_item: Dict[str, str]) -> Dict[str, int]: """Create a new item on the table Args: dynamodb_item (Dict[str, str]): Attributes for the item, must include the key specified on your table. Returns: Dict[str, int]: Status code of the results of the action """ try: validated_item: Dict[str, str] = self.validate_data( validation_type="new_item", unvalidated_data=dynamodb_item) formated_db_item: Dict[str, Dict[ str, str]] = self.validation.validate_item_to_db_format( validated_item) self.add_item(dynamodb_item=formated_db_item) return { "statusCode": 200, "body": f"Created new item with key: {validated_item[list(self.validation.key_template.keys())[0]]}", } except self.client_exceptions as error: return {"statusCode": 400, "body": str(error)} def delete_existing_attributes( self, key: Dict[str, str], validated_attributes: Dict[str, str]) -> Dict[str, str]: """Deletes any attributes that are the same as the old ones, used for update_item Args: key (Dict[str, str]): The key for the existing item validated_attributes (Dict[str, str]): Attributes that you want to update, parse in validated attributes only Returns: Dict[str, str]: Returns attributes that are only new """ existing_attributes: Dict[str, Dict[str, str]] = self.get_item(key=key) converted_existing_attributes: Dict[ str, str] = self.validation.validate_item_to_readable_format( dynamodb_item=existing_attributes) return self.remove_duplicated_attributes( new_attributes=validated_attributes, old_attributes=converted_existing_attributes, ) def generate_expressions( self, confirmed_new_attributes: Dict[str, str]) -> Tuple[str, Dict[str, str]]: """Used to generate expressions required to update an item in dynamodb Args: confirmed_new_attributes (Dict[str, str]): Only parse in the attributes after you've used delete_existing_attributes Returns: Tuple[str, Dict[str, str]]: Returns the update_expression string, attribute_names and attribute_values """ update_expression: str = self.generate_update_expression( new_attributes=confirmed_new_attributes, expression_mapping=self.validation.expression_mapping, ) expression_attribute_names: Dict[ str, str] = self.generate_expression_attribute_names( new_attributes=confirmed_new_attributes, expression_mapping=self.validation.expression_mapping, ) expression_attribute_values: Dict[ str, str] = self.generate_expression_attribute_values( new_attributes=confirmed_new_attributes, dynamodb_validation_format_mapper=self.validation. dynamodb_format_mapper, expression_mapping=self.validation.expression_mapping, ) return ( update_expression, expression_attribute_names, expression_attribute_values, ) def confirm_item_updated( self, update_response: Dict[str, Dict[str, str]], confirmed_new_attributes: Dict[str, str], ) -> Union[bool, Exception]: """Validates that the item has been updated successfully after you've pushed it through the database Args: update_response (Dict[str, Dict[str, str]]): The response from the database after you've made the change confirmed_new_attributes (Dict[str, str]): Parse in the attributes after using delete_existing_attributes Returns: Union[bool, Exception]: Returns if it's True or raises an exception if it fails to update """ formated_attributes: Dict[str, Dict[ str, str]] = self.validation.validate_item_to_db_format( dynamodb_item=confirmed_new_attributes) return self.validation.validate_attributes_updated( response=update_response, validated_new_attributes=formated_attributes) def update_item(self, dynamodb_attributes: Dict[str, str]) -> Dict[str, int]: """Updates an existing item to the database Args: dynamodb_attributes (Dict[str, str]): Provide your key along with the attribute names you want to update Returns: Dict[str, int]: Returns a status code and a body telling you if it passed or failed """ try: validated_attributes: Dict[str, str] = self.validate_data( validation_type="update_item", unvalidated_data=dynamodb_attributes) key: Dict[str, Dict[ str, str]] = self.validation.validate_item_to_db_format( dynamodb_item={ list(self.validation.key_template.keys())[0]: validated_attributes.pop( list(self.validation.key_template.keys())[0]) }) removed_duplicated_attributes: Dict[ str, str] = self.delete_existing_attributes( key=key, validated_attributes=validated_attributes) validated_new_attributes: Dict[ str, str] = self.validation.validate_new_attributes_exist( item_attributes=removed_duplicated_attributes) ( update_expression, expression_attribute_names, expression_attribute_values, ) = self.generate_expressions( confirmed_new_attributes=validated_new_attributes) update_response: Dict[str, str] = self.push_update( key=key, update_expression=update_expression, expression_attribute_names=expression_attribute_names, expression_attribute_values=expression_attribute_values, ) self.confirm_item_updated( update_response=update_response, confirmed_new_attributes=validated_new_attributes, ) return { "statusCode": 200, "body": "Item with the key provided has been updated successfully", } except self.client_exceptions as error: return {"statusCode": 400, "body": str(error)} def fetch_item( self, key: Dict[str, str]) -> Dict[str, Union[int, Dict[str, str]]]: """Get an existing item from the database Args: key (Dict[str, str]): They key for the item in the database Returns: Dict[str, Union[int, Dict[str, str]]]: Returns the status code and the item or the error why it failed """ try: validated_key: Dict[str, str] = self.validate_data( validation_type="read_item", unvalidated_data=key) formated_key: Dict[str, Dict[ str, str]] = self.validation.validate_item_to_db_format( dynamodb_item=validated_key) fetched_item: Dict[str, str] = self.get_item(key=formated_key) readable_item: Dict[ str, str] = self.validation.validate_item_to_readable_format( dynamodb_item=fetched_item) return {"statusCode": 200, "body": readable_item} except self.client_exceptions as error: return {"statusCode": 400, "body": str(error)} def fetch_items( self) -> Dict[str, Union[int, Union[str, List[Dict[str, str]]]]]: """Fetch a bulk amount of items from the database Returns: Dict[str, Union[int, Union[str, List[Dict[str, str]]]]]: Returns either a status code with a list of dictionaries or an error body """ try: unformated_table_items: List[Dict[str, Dict[str, str]]] = self.get_items() formated_table_items: List[Dict[str, str]] = [ self.validation.validate_item_to_readable_format(table_item) for table_item in unformated_table_items ] return {"statusCode": 200, "body": formated_table_items} except self.client_exceptions as error: return {"statusCode": 400, "body": str(error)} def delete_item(self, key: Dict[str, str]) -> Dict[str, int]: """Delete an existing item from the database Args: key (Dict[str, str]): The key for the item in the database Returns: Dict[str, int]: Returns a status code either passing or failing. """ try: validated_key: Dict[str, str] = self.validate_data( validation_type="delete_item", unvalidated_data=key) formated_key: Dict[str, Dict[ str, str]] = self.validation.validate_item_to_db_format( dynamodb_item=validated_key) self.remove_item(key=formated_key) return { "statusCode": 200, "body": f"Item with key: {validated_key[list(self.validation.key_template.keys())[0]]} has been deleted", } except self.client_exceptions as error: return {"statusCode": 400, "body": str(error)}
def test_creating_validation_class(self): validation = Validation(table_schema=self.generate_schema_template()) self.assertIsInstance(validation, Validation)
def test_validate_new_attributes_do_not_exist(self): validation: Validation = Validation( table_schema=self.generate_schema_template()) with self.assertRaises(ValidationNoNewAttributesError): validation.validate_new_attributes_exist(item_attributes={})