New methods to the databrew service for recipes and rulesets. (#4996)

This commit is contained in:
John-Paul Stanford 2022-04-07 10:45:17 +01:00 committed by GitHub
parent 552385881c
commit 2a6ba0ddd1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 411 additions and 4 deletions

View File

@ -15,6 +15,11 @@ class RecipeAlreadyExistsException(AlreadyExistsException):
super().__init__("Recipe") super().__init__("Recipe")
class RulesetAlreadyExistsException(AlreadyExistsException):
def __init__(self):
super().__init__("Ruleset")
class EntityNotFoundException(DataBrewClientError): class EntityNotFoundException(DataBrewClientError):
def __init__(self, msg): def __init__(self, msg):
super().__init__("EntityNotFoundException", msg) super().__init__("EntityNotFoundException", msg)
@ -23,3 +28,8 @@ class EntityNotFoundException(DataBrewClientError):
class RecipeNotFoundException(EntityNotFoundException): class RecipeNotFoundException(EntityNotFoundException):
def __init__(self, recipe_name): def __init__(self, recipe_name):
super().__init__("Recipe %s not found." % recipe_name) super().__init__("Recipe %s not found." % recipe_name)
class RulesetNotFoundException(EntityNotFoundException):
def __init__(self, recipe_name):
super().__init__("Ruleset %s not found." % recipe_name)

View File

@ -5,6 +5,7 @@ from moto.core import BaseBackend, BaseModel
from moto.core.utils import BackendDict from moto.core.utils import BackendDict
from moto.utilities.paginator import paginate from moto.utilities.paginator import paginate
from .exceptions import RecipeAlreadyExistsException, RecipeNotFoundException from .exceptions import RecipeAlreadyExistsException, RecipeNotFoundException
from .exceptions import RulesetAlreadyExistsException, RulesetNotFoundException
class DataBrewBackend(BaseBackend): class DataBrewBackend(BaseBackend):
@ -15,11 +16,18 @@ class DataBrewBackend(BaseBackend):
"limit_default": 100, "limit_default": 100,
"unique_attribute": "name", "unique_attribute": "name",
}, },
"list_rulesets": {
"input_token": "next_token",
"limit_key": "max_results",
"limit_default": 100,
"unique_attribute": "name",
},
} }
def __init__(self, region_name): def __init__(self, region_name):
self.region_name = region_name self.region_name = region_name
self.recipes = OrderedDict() self.recipes = OrderedDict()
self.rulesets = OrderedDict()
def reset(self): def reset(self):
"""Re-initialize all attributes for this instance.""" """Re-initialize all attributes for this instance."""
@ -37,6 +45,20 @@ class DataBrewBackend(BaseBackend):
self.recipes[recipe_name] = recipe self.recipes[recipe_name] = recipe
return recipe return recipe
def update_recipe(self, recipe_name, recipe_description, recipe_steps, tags):
if recipe_name not in self.recipes:
raise RecipeNotFoundException(recipe_name)
recipe = self.recipes[recipe_name]
if recipe_description is not None:
recipe.description = recipe_description
if recipe_steps is not None:
recipe.steps = recipe_steps
if tags is not None:
recipe.tags = tags
return recipe
@paginate(pagination_model=PAGINATION_MODEL) @paginate(pagination_model=PAGINATION_MODEL)
def list_recipes(self): def list_recipes(self):
return [self.recipes[key] for key in self.recipes] if self.recipes else [] return [self.recipes[key] for key in self.recipes] if self.recipes else []
@ -49,6 +71,52 @@ class DataBrewBackend(BaseBackend):
raise RecipeNotFoundException(recipe_name) raise RecipeNotFoundException(recipe_name)
return self.recipes[recipe_name] return self.recipes[recipe_name]
def create_ruleset(
self, ruleset_name, ruleset_description, ruleset_rules, ruleset_target_arn, tags
):
if ruleset_name in self.rulesets:
raise RulesetAlreadyExistsException()
ruleset = FakeRuleset(
self.region_name,
ruleset_name,
ruleset_description,
ruleset_rules,
ruleset_target_arn,
tags,
)
self.rulesets[ruleset_name] = ruleset
return ruleset
def update_ruleset(self, ruleset_name, ruleset_description, ruleset_rules, tags):
if ruleset_name not in self.rulesets:
raise RulesetNotFoundException(ruleset_name)
ruleset = self.rulesets[ruleset_name]
if ruleset_description is not None:
ruleset.description = ruleset_description
if ruleset_rules is not None:
ruleset.rules = ruleset_rules
if tags is not None:
ruleset.tags = tags
return ruleset
def get_ruleset(self, ruleset_name):
if ruleset_name not in self.rulesets:
raise RulesetNotFoundException(ruleset_name)
return self.rulesets[ruleset_name]
@paginate(pagination_model=PAGINATION_MODEL)
def list_rulesets(self):
return list(self.rulesets.values())
def delete_ruleset(self, ruleset_name):
if ruleset_name not in self.rulesets:
raise RulesetNotFoundException(ruleset_name)
del self.rulesets[ruleset_name]
class FakeRecipe(BaseModel): class FakeRecipe(BaseModel):
def __init__( def __init__(
@ -71,4 +139,34 @@ class FakeRecipe(BaseModel):
} }
class FakeRuleset(BaseModel):
def __init__(
self,
region_name,
ruleset_name,
ruleset_description,
ruleset_rules,
ruleset_target_arn,
tags,
):
self.region_name = region_name
self.name = ruleset_name
self.description = ruleset_description
self.rules = ruleset_rules
self.target_arn = ruleset_target_arn
self.created_time = datetime.now()
self.tags = tags
def as_dict(self):
return {
"Name": self.name,
"Rules": self.rules,
"Description": self.description,
"TargetArn": self.target_arn,
"CreateTime": self.created_time.isoformat(),
"Tags": self.tags or dict(),
}
databrew_backends = BackendDict(DataBrewBackend, "databrew") databrew_backends = BackendDict(DataBrewBackend, "databrew")

View File

@ -50,12 +50,98 @@ class DataBrewResponse(BaseResponse):
} }
) )
def put_recipe_response(self, recipe_name):
recipe_description = self.parameters.get("Description")
recipe_steps = self.parameters.get("Steps")
tags = self.parameters.get("Tags")
recipe = self.databrew_backend.update_recipe(
recipe_name, recipe_description, recipe_steps, tags
)
return 200, {}, json.dumps(recipe.as_dict())
def get_recipe_response(self, recipe_name):
recipe = self.databrew_backend.get_recipe(recipe_name)
return 201, {}, json.dumps(recipe.as_dict())
@amzn_request_id @amzn_request_id
def describe_recipe_response(self, request, full_url, headers): def recipe_response(self, request, full_url, headers):
self.setup_class(request, full_url, headers) self.setup_class(request, full_url, headers)
parsed_url = urlparse(full_url) parsed_url = urlparse(full_url)
recipe_name = parsed_url.path.rstrip("/").rsplit("/", 1)[1] recipe_name = parsed_url.path.rstrip("/").rsplit("/", 1)[1]
recipe = self.databrew_backend.get_recipe(recipe_name) if request.method == "PUT":
return json.dumps(recipe.as_dict()) return self.put_recipe_response(recipe_name)
elif request.method == "GET":
return self.get_recipe_response(recipe_name)
@amzn_request_id
def create_ruleset(self):
ruleset_description = self.parameters.get("Description")
ruleset_rules = self.parameters.get("Rules")
ruleset_name = self.parameters.get("Name")
ruleset_target_arn = self.parameters.get("TargetArn")
tags = self.parameters.get("Tags")
return json.dumps(
self.databrew_backend.create_ruleset(
ruleset_name,
ruleset_description,
ruleset_rules,
ruleset_target_arn,
tags,
).as_dict()
)
def put_ruleset_response(self, ruleset_name):
ruleset_description = self.parameters.get("Description")
ruleset_rules = self.parameters.get("Rules")
tags = self.parameters.get("Tags")
ruleset = self.databrew_backend.update_ruleset(
ruleset_name, ruleset_description, ruleset_rules, tags
)
return 200, {}, json.dumps(ruleset.as_dict())
def get_ruleset_response(self, ruleset_name):
ruleset = self.databrew_backend.get_ruleset(ruleset_name)
return 201, {}, json.dumps(ruleset.as_dict())
def delete_ruleset_response(self, ruleset_name):
self.databrew_backend.delete_ruleset(ruleset_name)
return 204, {}, ""
@amzn_request_id
def ruleset_response(self, request, full_url, headers):
self.setup_class(request, full_url, headers)
parsed_url = urlparse(full_url)
ruleset_name = parsed_url.path.split("/")[-1]
if request.method == "PUT":
response = self.put_ruleset_response(ruleset_name)
return response
elif request.method == "GET":
return self.get_ruleset_response(ruleset_name)
elif request.method == "DELETE":
return self.delete_ruleset_response(ruleset_name)
@amzn_request_id
def list_rulesets(self):
# https://docs.aws.amazon.com/databrew/latest/dg/API_ListRulesets.html
next_token = self._get_param("NextToken", self._get_param("nextToken"))
max_results = self._get_int_param(
"MaxResults", self._get_int_param("maxResults")
)
# pylint: disable=unexpected-keyword-arg, unbalanced-tuple-unpacking
ruleset_list, next_token = self.databrew_backend.list_rulesets(
next_token=next_token, max_results=max_results
)
return json.dumps(
{
"Rulesets": [ruleset.as_dict() for ruleset in ruleset_list],
"NextToken": next_token,
}
)

View File

@ -4,5 +4,7 @@ url_bases = [r"https?://databrew\.(.+)\.amazonaws.com"]
url_paths = { url_paths = {
"{0}/recipes$": DataBrewResponse.dispatch, "{0}/recipes$": DataBrewResponse.dispatch,
"{0}/recipes/(?P<recipe_name>[^/]+)$": DataBrewResponse().describe_recipe_response, "{0}/recipes/(?P<recipe_name>[^/]+)$": DataBrewResponse().recipe_response,
"{0}/rulesets$": DataBrewResponse.dispatch,
"{0}/rulesets/(?P<ruleset_name>[^/]+)$": DataBrewResponse().ruleset_response,
} }

View File

@ -96,6 +96,46 @@ def test_describe_recipe():
recipe["Steps"].should.have.length_of(1) recipe["Steps"].should.have.length_of(1)
@mock_databrew
def test_update_recipe():
client = _create_databrew_client()
response = _create_test_recipe(client)
recipe = client.update_recipe(
Name=response["Name"],
Steps=[
{
"Action": {
"Operation": "REMOVE_COMBINED",
"Parameters": {
"collapseConsecutiveWhitespace": "false",
"removeAllPunctuation": "false",
"removeAllQuotes": "false",
"removeAllWhitespace": "false",
"removeCustomCharacters": "true",
"removeCustomValue": "true",
"removeLeadingAndTrailingPunctuation": "false",
"removeLeadingAndTrailingQuotes": "false",
"removeLeadingAndTrailingWhitespace": "false",
"removeLetters": "false",
"removeNumbers": "false",
"removeSpecialCharacters": "true",
"sourceColumn": "FakeColumn",
},
}
}
],
)
recipe["Name"].should.equal(response["Name"])
# Describe the recipe and change the changes
recipe = client.describe_recipe(Name=response["Name"])
recipe["Name"].should.equal(response["Name"])
recipe["Steps"].should.have.length_of(1)
recipe["Steps"][0]["Action"]["Parameters"]["removeCustomValue"].should.equal("true")
@mock_databrew @mock_databrew
def test_describe_recipe_that_does_not_exist(): def test_describe_recipe_that_does_not_exist():
client = _create_databrew_client() client = _create_databrew_client()

View File

@ -0,0 +1,171 @@
import uuid
import boto3
import pytest
from botocore.exceptions import ClientError
from moto import mock_databrew
def _create_databrew_client():
client = boto3.client("databrew", region_name="us-west-1")
return client
def _create_test_ruleset(client, tags=None, ruleset_name=None):
if ruleset_name is None:
ruleset_name = str(uuid.uuid4())
return client.create_ruleset(
Name=ruleset_name,
TargetArn="arn:aws:databrew:eu-west-1:000000000000:dataset/fake-dataset",
Rules=[
{
"Name": "Assert values > 0",
"Disabled": False,
"CheckExpression": ":col1 > :val1",
"SubstitutionMap": {":col1": "`Value`", ":val1": "0"},
"Threshold": {
"Value": 100,
"Type": "GREATER_THAN_OR_EQUAL",
"Unit": "PERCENTAGE",
},
}
],
Tags=tags or {},
)
def _create_test_rulesets(client, count):
for _ in range(count):
_create_test_ruleset(client)
@mock_databrew
def test_ruleset_list_when_empty():
client = _create_databrew_client()
response = client.list_rulesets()
response.should.have.key("Rulesets")
response["Rulesets"].should.have.length_of(0)
@mock_databrew
def test_list_ruleset_with_max_results():
client = _create_databrew_client()
_create_test_rulesets(client, 4)
response = client.list_rulesets(MaxResults=2)
response["Rulesets"].should.have.length_of(2)
response.should.have.key("NextToken")
@mock_databrew
def test_list_rulesets_from_next_token():
client = _create_databrew_client()
_create_test_rulesets(client, 10)
first_response = client.list_rulesets(MaxResults=3)
response = client.list_rulesets(NextToken=first_response["NextToken"])
response["Rulesets"].should.have.length_of(7)
@mock_databrew
def test_list_rulesets_with_max_results_greater_than_actual_results():
client = _create_databrew_client()
_create_test_rulesets(client, 4)
response = client.list_rulesets(MaxResults=10)
response["Rulesets"].should.have.length_of(4)
@mock_databrew
def test_describe_ruleset():
client = _create_databrew_client()
response = _create_test_ruleset(client)
ruleset = client.describe_ruleset(Name=response["Name"])
ruleset["Name"].should.equal(response["Name"])
ruleset["Rules"].should.have.length_of(1)
@mock_databrew
def test_describe_ruleset_that_does_not_exist():
client = _create_databrew_client()
with pytest.raises(ClientError) as exc:
client.describe_ruleset(Name="DoseNotExist")
err = exc.value.response["Error"]
err["Code"].should.equal("EntityNotFoundException")
err["Message"].should.equal("Ruleset DoseNotExist not found.")
@mock_databrew
def test_create_ruleset_that_already_exists():
client = _create_databrew_client()
response = _create_test_ruleset(client)
with pytest.raises(ClientError) as exc:
_create_test_ruleset(client, ruleset_name=response["Name"])
err = exc.value.response["Error"]
err["Code"].should.equal("AlreadyExistsException")
err["Message"].should.equal("Ruleset already exists.")
@mock_databrew
def test_delete_ruleset():
client = _create_databrew_client()
response = _create_test_ruleset(client)
# Check ruleset exists
ruleset = client.describe_ruleset(Name=response["Name"])
ruleset["Name"].should.equal(response["Name"])
# Delete the ruleset
client.delete_ruleset(Name=response["Name"])
# Check it does not exist anymore
with pytest.raises(ClientError) as exc:
client.describe_ruleset(Name=response["Name"])
err = exc.value.response["Error"]
err["Code"].should.equal("EntityNotFoundException")
err["Message"].should.equal(f"Ruleset {response['Name']} not found.")
# Check that a ruleset that does not exist errors
with pytest.raises(ClientError) as exc:
client.delete_ruleset(Name=response["Name"])
err = exc.value.response["Error"]
err["Code"].should.equal("EntityNotFoundException")
err["Message"].should.equal(f"Ruleset {response['Name']} not found.")
@mock_databrew
def test_update_ruleset():
client = _create_databrew_client()
response = _create_test_ruleset(client)
# Update the ruleset and check response
ruleset = client.update_ruleset(
Name=response["Name"],
Rules=[
{
"Name": "Assert values > 0",
"Disabled": False,
"CheckExpression": ":col1 > :val1",
"SubstitutionMap": {":col1": "`Value`", ":val1": "10"},
"Threshold": {
"Value": 100,
"Type": "GREATER_THAN_OR_EQUAL",
"Unit": "PERCENTAGE",
},
}
],
)
ruleset["Name"].should.equal(response["Name"])
# Describe the ruleset and check the changes
ruleset = client.describe_ruleset(Name=response["Name"])
ruleset["Name"].should.equal(response["Name"])
ruleset["Rules"].should.have.length_of(1)
ruleset["Rules"][0]["SubstitutionMap"][":val1"].should.equal("10")