Databrew: add recipe version support (#5094)

This commit is contained in:
Michael Sanders 2022-05-05 21:33:33 +01:00 committed by GitHub
parent a666d59b58
commit 3f89b98889
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 685 additions and 59 deletions

View File

@ -1069,7 +1069,7 @@
## databrew ## databrew
<details> <details>
<summary>15% implemented</summary> <summary>22% implemented</summary>
- [ ] batch_delete_recipe_version - [ ] batch_delete_recipe_version
- [ ] create_dataset - [ ] create_dataset
@ -1082,26 +1082,26 @@
- [ ] delete_dataset - [ ] delete_dataset
- [ ] delete_job - [ ] delete_job
- [ ] delete_project - [ ] delete_project
- [ ] delete_recipe_version - [X] delete_recipe_version
- [X] delete_ruleset - [X] delete_ruleset
- [ ] delete_schedule - [ ] delete_schedule
- [ ] describe_dataset - [ ] describe_dataset
- [ ] describe_job - [ ] describe_job
- [ ] describe_job_run - [ ] describe_job_run
- [ ] describe_project - [ ] describe_project
- [ ] describe_recipe - [X] describe_recipe
- [ ] describe_ruleset - [ ] describe_ruleset
- [ ] describe_schedule - [ ] describe_schedule
- [ ] list_datasets - [ ] list_datasets
- [ ] list_job_runs - [ ] list_job_runs
- [ ] list_jobs - [ ] list_jobs
- [ ] list_projects - [ ] list_projects
- [ ] list_recipe_versions - [X] list_recipe_versions
- [X] list_recipes - [X] list_recipes
- [X] list_rulesets - [X] list_rulesets
- [ ] list_schedules - [ ] list_schedules
- [ ] list_tags_for_resource - [ ] list_tags_for_resource
- [ ] publish_recipe - [X] publish_recipe
- [ ] send_project_session_action - [ ] send_project_session_action
- [ ] start_job_run - [ ] start_job_run
- [ ] start_project_session - [ ] start_project_session

View File

@ -36,26 +36,26 @@ databrew
- [ ] delete_dataset - [ ] delete_dataset
- [ ] delete_job - [ ] delete_job
- [ ] delete_project - [ ] delete_project
- [ ] delete_recipe_version - [X] delete_recipe_version
- [X] delete_ruleset - [X] delete_ruleset
- [ ] delete_schedule - [ ] delete_schedule
- [ ] describe_dataset - [ ] describe_dataset
- [ ] describe_job - [ ] describe_job
- [ ] describe_job_run - [ ] describe_job_run
- [ ] describe_project - [ ] describe_project
- [ ] describe_recipe - [X] describe_recipe
- [ ] describe_ruleset - [ ] describe_ruleset
- [ ] describe_schedule - [ ] describe_schedule
- [ ] list_datasets - [ ] list_datasets
- [ ] list_job_runs - [ ] list_job_runs
- [ ] list_jobs - [ ] list_jobs
- [ ] list_projects - [ ] list_projects
- [ ] list_recipe_versions - [X] list_recipe_versions
- [X] list_recipes - [X] list_recipes
- [X] list_rulesets - [X] list_rulesets
- [ ] list_schedules - [ ] list_schedules
- [ ] list_tags_for_resource - [ ] list_tags_for_resource
- [ ] publish_recipe - [X] publish_recipe
- [ ] send_project_session_action - [ ] send_project_session_action
- [ ] start_job_run - [ ] start_job_run
- [ ] start_project_session - [ ] start_project_session

View File

@ -10,9 +10,16 @@ class AlreadyExistsException(DataBrewClientError):
super().__init__("AlreadyExistsException", "%s already exists." % (typ)) super().__init__("AlreadyExistsException", "%s already exists." % (typ))
class RecipeAlreadyExistsException(AlreadyExistsException): class ConflictException(DataBrewClientError):
def __init__(self): code = 409
super().__init__("Recipe")
def __init__(self, message, **kwargs):
super().__init__("ConflictException", message, **kwargs)
class ValidationException(DataBrewClientError):
def __init__(self, message, **kwargs):
super().__init__("ValidationException", message, **kwargs)
class RulesetAlreadyExistsException(AlreadyExistsException): class RulesetAlreadyExistsException(AlreadyExistsException):
@ -25,9 +32,11 @@ class EntityNotFoundException(DataBrewClientError):
super().__init__("EntityNotFoundException", msg) super().__init__("EntityNotFoundException", msg)
class RecipeNotFoundException(EntityNotFoundException): class ResourceNotFoundException(DataBrewClientError):
def __init__(self, recipe_name): code = 404
super().__init__("Recipe %s not found." % recipe_name)
def __init__(self, message, **kwargs):
super().__init__("ResourceNotFoundException", message, **kwargs)
class RulesetNotFoundException(EntityNotFoundException): class RulesetNotFoundException(EntityNotFoundException):

View File

@ -1,10 +1,16 @@
from collections import OrderedDict from collections import OrderedDict
from copy import deepcopy
import math
from datetime import datetime from datetime import datetime
from moto.core import BaseBackend, BaseModel 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 (
ConflictException,
ResourceNotFoundException,
ValidationException,
)
from .exceptions import RulesetAlreadyExistsException, RulesetNotFoundException from .exceptions import RulesetAlreadyExistsException, RulesetNotFoundException
@ -16,6 +22,12 @@ class DataBrewBackend(BaseBackend):
"limit_default": 100, "limit_default": 100,
"unique_attribute": "name", "unique_attribute": "name",
}, },
"list_recipe_versions": {
"input_token": "next_token",
"limit_key": "max_results",
"limit_default": 100,
"unique_attribute": "name",
},
"list_rulesets": { "list_rulesets": {
"input_token": "next_token", "input_token": "next_token",
"limit_key": "max_results", "limit_key": "max_results",
@ -34,42 +46,134 @@ class DataBrewBackend(BaseBackend):
region_name = self.region_name region_name = self.region_name
self.__init__(region_name) self.__init__(region_name)
@staticmethod
def validate_length(param, param_name, max_length):
if len(param) > max_length:
raise ValidationException(
f"1 validation error detected: Value '{param}' at '{param_name}' failed to "
f"satisfy constraint: Member must have length less than or equal to {max_length}"
)
def create_recipe(self, recipe_name, recipe_description, recipe_steps, tags): def create_recipe(self, recipe_name, recipe_description, recipe_steps, tags):
# https://docs.aws.amazon.com/databrew/latest/dg/API_CreateRecipe.html # https://docs.aws.amazon.com/databrew/latest/dg/API_CreateRecipe.html
if recipe_name in self.recipes: if recipe_name in self.recipes:
raise RecipeAlreadyExistsException() raise ConflictException(f"The recipe {recipe_name} already exists")
recipe = FakeRecipe( recipe = FakeRecipe(
self.region_name, recipe_name, recipe_description, recipe_steps, tags self.region_name, recipe_name, recipe_description, recipe_steps, tags
) )
self.recipes[recipe_name] = recipe self.recipes[recipe_name] = recipe
return recipe return recipe.latest_working
def update_recipe(self, recipe_name, recipe_description, recipe_steps, tags): def delete_recipe_version(self, recipe_name, recipe_version):
if not FakeRecipe.version_is_valid(recipe_version, latest_published=False):
raise ValidationException(
f"Recipe {recipe_name} version {recipe_version} is invalid."
)
try:
recipe = self.recipes[recipe_name]
except KeyError:
raise ResourceNotFoundException(f"The recipe {recipe_name} wasn't found")
if (
recipe_version != FakeRecipe.LATEST_WORKING
and float(recipe_version) not in recipe.versions
):
raise ResourceNotFoundException(
f"The recipe {recipe_name} version {recipe_version } wasn't found."
)
if recipe_version in (
FakeRecipe.LATEST_WORKING,
str(recipe.latest_working.version),
):
if recipe.latest_published is not None:
# Can only delete latest working version when there are no others
raise ValidationException(
f"Recipe version {recipe_version} is not allowed to be deleted"
)
else:
del self.recipes[recipe_name]
else:
recipe.delete_published_version(recipe_version)
def update_recipe(self, recipe_name, recipe_description, recipe_steps):
if recipe_name not in self.recipes: if recipe_name not in self.recipes:
raise RecipeNotFoundException(recipe_name) raise ResourceNotFoundException(f"The recipe {recipe_name} wasn't found")
recipe = self.recipes[recipe_name] recipe = self.recipes[recipe_name]
if recipe_description is not None: recipe.update(recipe_description, recipe_steps)
recipe.description = recipe_description
if recipe_steps is not None:
recipe.steps = recipe_steps
if tags is not None:
recipe.tags = tags
return recipe return recipe.latest_working
@paginate(pagination_model=PAGINATION_MODEL) @paginate(pagination_model=PAGINATION_MODEL)
def list_recipes(self): def list_recipes(self, recipe_version=None):
return [self.recipes[key] for key in self.recipes] if self.recipes else [] # https://docs.aws.amazon.com/databrew/latest/dg/API_ListRecipes.html
if recipe_version == FakeRecipe.LATEST_WORKING:
version = "latest_working"
elif recipe_version in (None, FakeRecipe.LATEST_PUBLISHED):
version = "latest_published"
else:
raise ValidationException(
f"Invalid version {recipe_version}. "
"Valid versions are LATEST_PUBLISHED and LATEST_WORKING."
)
recipes = [getattr(self.recipes[key], version) for key in self.recipes]
return [r for r in recipes if r is not None]
def get_recipe(self, recipe_name): @paginate(pagination_model=PAGINATION_MODEL)
""" def list_recipe_versions(self, recipe_name):
The Version-parameter has not yet been implemented # https://docs.aws.amazon.com/databrew/latest/dg/API_ListRecipeVersions.html
""" self.validate_length(recipe_name, "name", 255)
if recipe_name not in self.recipes:
raise RecipeNotFoundException(recipe_name) recipe = self.recipes.get(recipe_name)
return self.recipes[recipe_name] if recipe is None:
return []
latest_working = recipe.latest_working
recipe_versions = [
recipe_version
for recipe_version in recipe.versions.values()
if recipe_version is not latest_working
]
return [r for r in recipe_versions if r is not None]
def get_recipe(self, recipe_name, recipe_version=None):
# https://docs.aws.amazon.com/databrew/latest/dg/API_DescribeRecipe.html
self.validate_length(recipe_name, "name", 255)
if recipe_version is None:
recipe_version = FakeRecipe.LATEST_PUBLISHED
else:
self.validate_length(recipe_version, "recipeVersion", 16)
if not FakeRecipe.version_is_valid(recipe_version):
raise ValidationException(
f"Recipe {recipe_name} version {recipe_version} isn't valid."
)
recipe = None
if recipe_name in self.recipes:
if recipe_version == FakeRecipe.LATEST_PUBLISHED:
recipe = self.recipes[recipe_name].latest_published
elif recipe_version == FakeRecipe.LATEST_WORKING:
recipe = self.recipes[recipe_name].latest_working
else:
recipe = self.recipes[recipe_name].versions.get(float(recipe_version))
if recipe is None:
raise ResourceNotFoundException(
f"The recipe {recipe_name} for version {recipe_version} wasn't found."
)
return recipe
def publish_recipe(self, recipe_name, description=None):
# https://docs.aws.amazon.com/databrew/latest/dg/API_PublishRecipe.html
self.validate_length(recipe_name, "name", 255)
try:
self.recipes[recipe_name].publish(description)
except KeyError:
raise ResourceNotFoundException(f"Recipe {recipe_name} wasn't found")
def create_ruleset( def create_ruleset(
self, ruleset_name, ruleset_description, ruleset_rules, ruleset_target_arn, tags self, ruleset_name, ruleset_description, ruleset_rules, ruleset_target_arn, tags
@ -119,8 +223,77 @@ class DataBrewBackend(BaseBackend):
class FakeRecipe(BaseModel): class FakeRecipe(BaseModel):
INITIAL_VERSION = 0.1
LATEST_WORKING = "LATEST_WORKING"
LATEST_PUBLISHED = "LATEST_PUBLISHED"
@classmethod
def version_is_valid(cls, version, latest_working=True, latest_published=True):
validity = True
if len(version) < 1 or len(version) > 16:
validity = False
else:
try:
version = float(version)
except ValueError:
if not (
(version == cls.LATEST_WORKING and latest_working)
or (version == cls.LATEST_PUBLISHED and latest_published)
):
validity = False
return validity
def __init__( def __init__(
self, region_name, recipe_name, recipe_description, recipe_steps, tags self, region_name, recipe_name, recipe_description, recipe_steps, tags
):
self.versions = OrderedDict()
self.versions[self.INITIAL_VERSION] = FakeRecipeVersion(
region_name,
recipe_name,
recipe_description,
recipe_steps,
tags,
version=self.INITIAL_VERSION,
)
self.latest_working = self.versions[self.INITIAL_VERSION]
self.latest_published = None
def publish(self, description=None):
self.latest_published = self.latest_working
self.latest_working = deepcopy(self.latest_working)
self.latest_published.publish(description)
del self.versions[self.latest_working.version]
self.versions[self.latest_published.version] = self.latest_published
self.latest_working.version = self.latest_published.version + 0.1
self.versions[self.latest_working.version] = self.latest_working
def update(self, description, steps):
if description is not None:
self.latest_working.description = description
if steps is not None:
self.latest_working.steps = steps
def delete_published_version(self, version):
version = float(version)
assert version.is_integer()
if version == self.latest_published.version:
prev_version = version - 1.0
# Iterate back through the published versions until we find one that exists
while prev_version >= 1.0:
if prev_version in self.versions:
self.latest_published = self.versions[prev_version]
break
prev_version -= 1.0
else:
# Didn't find an earlier published version
self.latest_published = None
del self.versions[version]
class FakeRecipeVersion(BaseModel):
def __init__(
self, region_name, recipe_name, recipe_description, recipe_steps, tags, version
): ):
self.region_name = region_name self.region_name = region_name
self.name = recipe_name self.name = recipe_name
@ -128,15 +301,28 @@ class FakeRecipe(BaseModel):
self.steps = recipe_steps self.steps = recipe_steps
self.created_time = datetime.now() self.created_time = datetime.now()
self.tags = tags self.tags = tags
self.published_date = None
self.version = version
def as_dict(self): def as_dict(self):
return { dict_recipe = {
"Name": self.name, "Name": self.name,
"Steps": self.steps, "Steps": self.steps,
"Description": self.description, "Description": self.description,
"CreateTime": self.created_time.isoformat(), "CreateDate": "%.3f" % self.created_time.timestamp(),
"Tags": self.tags or dict(), "Tags": self.tags or dict(),
"RecipeVersion": str(self.version),
} }
if self.published_date is not None:
dict_recipe["PublishedDate"] = "%.3f" % self.published_date.timestamp()
return dict_recipe
def publish(self, description):
self.version = float(math.ceil(self.version))
self.published_date = datetime.now()
if description is not None:
self.description = description
class FakeRuleset(BaseModel): class FakeRuleset(BaseModel):

View File

@ -31,6 +31,22 @@ class DataBrewResponse(BaseResponse):
).as_dict() ).as_dict()
) )
@amzn_request_id
def delete_recipe_version(self, request, full_url, headers):
self.setup_class(request, full_url, headers)
# https://docs.aws.amazon.com/databrew/latest/dg/API_DeleteRecipeVersion.html
if request.method == "DELETE":
parsed_url = urlparse(full_url)
split_path = parsed_url.path.strip("/").split("/")
recipe_name = split_path[1]
recipe_version = split_path[3]
self.databrew_backend.delete_recipe_version(recipe_name, recipe_version)
return (
200,
{},
json.dumps({"Name": recipe_name, "RecipeVersion": recipe_version}),
)
@amzn_request_id @amzn_request_id
def list_recipes(self): def list_recipes(self):
# https://docs.aws.amazon.com/databrew/latest/dg/API_ListRecipes.html # https://docs.aws.amazon.com/databrew/latest/dg/API_ListRecipes.html
@ -38,10 +54,15 @@ class DataBrewResponse(BaseResponse):
max_results = self._get_int_param( max_results = self._get_int_param(
"MaxResults", self._get_int_param("maxResults") "MaxResults", self._get_int_param("maxResults")
) )
recipe_version = self._get_param(
"RecipeVersion", self._get_param("recipeVersion")
)
# pylint: disable=unexpected-keyword-arg, unbalanced-tuple-unpacking # pylint: disable=unexpected-keyword-arg, unbalanced-tuple-unpacking
recipe_list, next_token = self.databrew_backend.list_recipes( recipe_list, next_token = self.databrew_backend.list_recipes(
next_token=next_token, max_results=max_results next_token=next_token,
max_results=max_results,
recipe_version=recipe_version,
) )
return json.dumps( return json.dumps(
{ {
@ -50,19 +71,55 @@ class DataBrewResponse(BaseResponse):
} }
) )
@amzn_request_id
def list_recipe_versions(self, request, full_url, headers):
# https://docs.aws.amazon.com/databrew/latest/dg/API_ListRecipeVersions.html
self.setup_class(request, full_url, headers)
recipe_name = self._get_param("Name", self._get_param("name"))
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
recipe_list, next_token = self.databrew_backend.list_recipe_versions(
recipe_name=recipe_name, next_token=next_token, max_results=max_results
)
return json.dumps(
{
"Recipes": [recipe.as_dict() for recipe in recipe_list],
"NextToken": next_token,
}
)
@amzn_request_id
def publish_recipe(self, request, full_url, headers):
self.setup_class(request, full_url, headers)
if request.method == "POST":
parsed_url = urlparse(full_url)
recipe_name = parsed_url.path.strip("/").split("/", 2)[1]
recipe_description = self.parameters.get("Description")
self.databrew_backend.publish_recipe(recipe_name, recipe_description)
return 200, {}, json.dumps({"Name": recipe_name})
def put_recipe_response(self, recipe_name): def put_recipe_response(self, recipe_name):
recipe_description = self.parameters.get("Description") recipe_description = self.parameters.get("Description")
recipe_steps = self.parameters.get("Steps") recipe_steps = self.parameters.get("Steps")
tags = self.parameters.get("Tags")
recipe = self.databrew_backend.update_recipe( self.databrew_backend.update_recipe(
recipe_name, recipe_description, recipe_steps, tags recipe_name, recipe_description, recipe_steps
) )
return 200, {}, json.dumps(recipe.as_dict()) return 200, {}, json.dumps({"Name": recipe_name})
def get_recipe_response(self, recipe_name): def get_recipe_response(self, recipe_name):
recipe = self.databrew_backend.get_recipe(recipe_name) # https://docs.aws.amazon.com/databrew/latest/dg/API_DescribeRecipe.html
return 201, {}, json.dumps(recipe.as_dict()) recipe_version = self._get_param(
"RecipeVersion", self._get_param("recipeVersion")
)
recipe = self.databrew_backend.get_recipe(
recipe_name, recipe_version=recipe_version
)
return 200, {}, json.dumps(recipe.as_dict())
@amzn_request_id @amzn_request_id
def recipe_response(self, request, full_url, headers): def recipe_response(self, request, full_url, headers):

View File

@ -3,8 +3,11 @@ from .responses import DataBrewResponse
url_bases = [r"https?://databrew\.(.+)\.amazonaws.com"] url_bases = [r"https?://databrew\.(.+)\.amazonaws.com"]
url_paths = { url_paths = {
"{0}/recipeVersions$": DataBrewResponse().list_recipe_versions,
"{0}/recipes$": DataBrewResponse.dispatch, "{0}/recipes$": DataBrewResponse.dispatch,
"{0}/recipes/(?P<recipe_name>[^/]+)$": DataBrewResponse().recipe_response, "{0}/recipes/(?P<recipe_name>[^/]+)$": DataBrewResponse().recipe_response,
"{0}/recipes/(?P<recipe_name>[^/]+)/recipeVersion/(?P<recipe_version>[^/]+)": DataBrewResponse().delete_recipe_version,
"{0}/recipes/(?P<recipe_name>[^/]+)/publishRecipe$": DataBrewResponse().publish_recipe,
"{0}/rulesets$": DataBrewResponse.dispatch, "{0}/rulesets$": DataBrewResponse.dispatch,
"{0}/rulesets/(?P<ruleset_name>[^/]+)$": DataBrewResponse().ruleset_response, "{0}/rulesets/(?P<ruleset_name>[^/]+)$": DataBrewResponse().ruleset_response,
} }

View File

@ -3,6 +3,7 @@ import uuid
import boto3 import boto3
import pytest import pytest
from botocore.exceptions import ClientError from botocore.exceptions import ClientError
from datetime import datetime
from moto import mock_databrew from moto import mock_databrew
@ -58,12 +59,30 @@ def test_recipe_list_when_empty():
response["Recipes"].should.have.length_of(0) response["Recipes"].should.have.length_of(0)
@mock_databrew
def test_recipe_list_with_invalid_version():
client = _create_databrew_client()
recipe_version = "1.1"
with pytest.raises(ClientError) as exc:
client.list_recipes(RecipeVersion=recipe_version)
err = exc.value.response["Error"]
err["Code"].should.equal("ValidationException")
err["Message"].should.equal(
f"Invalid version {recipe_version}. "
"Valid versions are LATEST_PUBLISHED and LATEST_WORKING."
)
exc.value.response["ResponseMetadata"]["HTTPStatusCode"].should.equal(400)
err = exc.value.response["Error"]
err["Code"].should.equal("ValidationException")
@mock_databrew @mock_databrew
def test_list_recipes_with_max_results(): def test_list_recipes_with_max_results():
client = _create_databrew_client() client = _create_databrew_client()
_create_test_recipes(client, 4) _create_test_recipes(client, 4)
response = client.list_recipes(MaxResults=2) response = client.list_recipes(MaxResults=2, RecipeVersion="LATEST_WORKING")
response["Recipes"].should.have.length_of(2) response["Recipes"].should.have.length_of(2)
response.should.have.key("NextToken") response.should.have.key("NextToken")
@ -72,8 +91,10 @@ def test_list_recipes_with_max_results():
def test_list_recipes_from_next_token(): def test_list_recipes_from_next_token():
client = _create_databrew_client() client = _create_databrew_client()
_create_test_recipes(client, 10) _create_test_recipes(client, 10)
first_response = client.list_recipes(MaxResults=3) first_response = client.list_recipes(MaxResults=3, RecipeVersion="LATEST_WORKING")
response = client.list_recipes(NextToken=first_response["NextToken"]) response = client.list_recipes(
NextToken=first_response["NextToken"], RecipeVersion="LATEST_WORKING"
)
response["Recipes"].should.have.length_of(7) response["Recipes"].should.have.length_of(7)
@ -81,19 +102,160 @@ def test_list_recipes_from_next_token():
def test_list_recipes_with_max_results_greater_than_actual_results(): def test_list_recipes_with_max_results_greater_than_actual_results():
client = _create_databrew_client() client = _create_databrew_client()
_create_test_recipes(client, 4) _create_test_recipes(client, 4)
response = client.list_recipes(MaxResults=10) response = client.list_recipes(MaxResults=10, RecipeVersion="LATEST_WORKING")
response["Recipes"].should.have.length_of(4) response["Recipes"].should.have.length_of(4)
@mock_databrew @mock_databrew
def test_describe_recipe(): def test_list_recipe_versions_no_recipe():
client = _create_databrew_client()
recipe_name = "NotExist"
response = client.list_recipe_versions(Name=recipe_name)
response["Recipes"].should.have.length_of(0)
@mock_databrew
def test_list_recipe_versions_none_published():
client = _create_databrew_client()
response = _create_test_recipe(client)
recipe_name = response["Name"]
response = client.list_recipe_versions(Name=recipe_name)
response["Recipes"].should.have.length_of(0)
@mock_databrew
def test_list_recipe_versions_one_published():
client = _create_databrew_client()
response = _create_test_recipe(client)
recipe_name = response["Name"]
client.publish_recipe(Name=recipe_name)
response = client.list_recipe_versions(Name=recipe_name)
response["Recipes"].should.have.length_of(1)
response["Recipes"][0]["RecipeVersion"].should.equal("1.0")
@mock_databrew
def test_list_recipe_versions_two_published():
client = _create_databrew_client()
response = _create_test_recipe(client)
recipe_name = response["Name"]
client.publish_recipe(Name=recipe_name)
client.publish_recipe(Name=recipe_name)
response = client.list_recipe_versions(Name=recipe_name)
response["Recipes"].should.have.length_of(2)
response["Recipes"][0]["RecipeVersion"].should.equal("1.0")
response["Recipes"][1]["RecipeVersion"].should.equal("2.0")
@mock_databrew
def test_describe_recipe_latest_working():
client = _create_databrew_client() client = _create_databrew_client()
response = _create_test_recipe(client) response = _create_test_recipe(client)
recipe = client.describe_recipe(
Name=response["Name"], RecipeVersion="LATEST_WORKING"
)
recipe["Name"].should.equal(response["Name"])
recipe["Steps"].should.have.length_of(1)
recipe["RecipeVersion"].should.equal("0.1")
@mock_databrew
def test_describe_recipe_with_version():
client = _create_databrew_client()
response = _create_test_recipe(client)
recipe = client.describe_recipe(Name=response["Name"], RecipeVersion="0.1")
recipe["Name"].should.equal(response["Name"])
recipe["Steps"].should.have.length_of(1)
recipe["RecipeVersion"].should.equal("0.1")
@mock_databrew
def test_describe_recipe_latest_published():
client = _create_databrew_client()
response = _create_test_recipe(client)
client.publish_recipe(Name=response["Name"])
recipe = client.describe_recipe(
Name=response["Name"], RecipeVersion="LATEST_PUBLISHED"
)
recipe["Name"].should.equal(response["Name"])
recipe["Steps"].should.have.length_of(1)
recipe["RecipeVersion"].should.equal("1.0")
@mock_databrew
def test_describe_recipe_implicit_latest_published():
client = _create_databrew_client()
response = _create_test_recipe(client)
client.publish_recipe(Name=response["Name"])
recipe = client.describe_recipe(Name=response["Name"]) recipe = client.describe_recipe(Name=response["Name"])
recipe["Name"].should.equal(response["Name"]) recipe["Name"].should.equal(response["Name"])
recipe["Steps"].should.have.length_of(1) recipe["Steps"].should.have.length_of(1)
recipe["RecipeVersion"].should.equal("1.0")
@mock_databrew
def test_describe_recipe_that_does_not_exist():
client = _create_databrew_client()
with pytest.raises(ClientError) as exc:
client.describe_recipe(Name="DoseNotExist")
err = exc.value.response["Error"]
err["Code"].should.equal("ResourceNotFoundException")
err["Message"].should.equal(
"The recipe DoseNotExist for version LATEST_PUBLISHED wasn't found."
)
exc.value.response["ResponseMetadata"]["HTTPStatusCode"].should.equal(404)
@mock_databrew
def test_describe_recipe_with_long_name():
client = _create_databrew_client()
name = "a" * 256
with pytest.raises(ClientError) as exc:
client.describe_recipe(Name=name)
err = exc.value.response["Error"]
err["Code"].should.equal("ValidationException")
err["Message"].should.equal(
f"1 validation error detected: Value '{name}' at 'name' failed to satisfy constraint: "
f"Member must have length less than or equal to 255"
)
exc.value.response["ResponseMetadata"]["HTTPStatusCode"].should.equal(400)
@mock_databrew
def test_describe_recipe_with_long_version():
client = _create_databrew_client()
version = "1" * 17
with pytest.raises(ClientError) as exc:
client.describe_recipe(Name="AnyName", RecipeVersion=version)
err = exc.value.response["Error"]
err["Code"].should.equal("ValidationException")
err["Message"].should.equal(
f"1 validation error detected: Value '{version}' at 'recipeVersion' failed to satisfy constraint: "
f"Member must have length less than or equal to 16"
)
exc.value.response["ResponseMetadata"]["HTTPStatusCode"].should.equal(400)
@mock_databrew
def test_describe_recipe_with_invalid_version():
client = _create_databrew_client()
name = "AnyName"
version = "invalid"
with pytest.raises(ClientError) as exc:
client.describe_recipe(Name=name, RecipeVersion=version)
err = exc.value.response["Error"]
err["Code"].should.equal("ValidationException")
err["Message"].should.equal(f"Recipe {name} version {version} isn't valid.")
exc.value.response["ResponseMetadata"]["HTTPStatusCode"].should.equal(400)
@mock_databrew @mock_databrew
@ -129,22 +291,46 @@ def test_update_recipe():
recipe["Name"].should.equal(response["Name"]) recipe["Name"].should.equal(response["Name"])
# Describe the recipe and change the changes # Describe the recipe and check the changes
recipe = client.describe_recipe(Name=response["Name"]) recipe = client.describe_recipe(
Name=response["Name"], RecipeVersion="LATEST_WORKING"
)
recipe["Name"].should.equal(response["Name"]) recipe["Name"].should.equal(response["Name"])
recipe["Steps"].should.have.length_of(1) recipe["Steps"].should.have.length_of(1)
recipe["Steps"][0]["Action"]["Parameters"]["removeCustomValue"].should.equal("true") recipe["Steps"][0]["Action"]["Parameters"]["removeCustomValue"].should.equal("true")
@mock_databrew @mock_databrew
def test_describe_recipe_that_does_not_exist(): def test_update_recipe_description():
client = _create_databrew_client()
response = _create_test_recipe(client)
description = "NewDescription"
recipe = client.update_recipe(
Name=response["Name"], Steps=[], Description=description
)
recipe["Name"].should.equal(response["Name"])
# Describe the recipe and check the changes
recipe = client.describe_recipe(
Name=response["Name"], RecipeVersion="LATEST_WORKING"
)
recipe["Name"].should.equal(response["Name"])
recipe["Description"].should.equal(description)
@mock_databrew
def test_update_recipe_invalid():
client = _create_databrew_client() client = _create_databrew_client()
recipe_name = "NotFound"
with pytest.raises(ClientError) as exc: with pytest.raises(ClientError) as exc:
client.describe_recipe(Name="DoseNotExist") client.update_recipe(Name=recipe_name)
err = exc.value.response["Error"] err = exc.value.response["Error"]
err["Code"].should.equal("EntityNotFoundException") err["Code"].should.equal("ResourceNotFoundException")
err["Message"].should.equal("Recipe DoseNotExist not found.") err["Message"].should.equal(f"The recipe {recipe_name} wasn't found")
exc.value.response["ResponseMetadata"]["HTTPStatusCode"].should.equal(404)
@mock_databrew @mock_databrew
@ -152,9 +338,194 @@ def test_create_recipe_that_already_exists():
client = _create_databrew_client() client = _create_databrew_client()
response = _create_test_recipe(client) response = _create_test_recipe(client)
recipe_name = response["Name"]
with pytest.raises(ClientError) as exc: with pytest.raises(ClientError) as exc:
_create_test_recipe(client, recipe_name=response["Name"]) _create_test_recipe(client, recipe_name=response["Name"])
err = exc.value.response["Error"] err = exc.value.response["Error"]
err["Code"].should.equal("AlreadyExistsException") err["Code"].should.equal("ConflictException")
err["Message"].should.equal("Recipe already exists.") err["Message"].should.equal(f"The recipe {recipe_name} already exists")
exc.value.response["ResponseMetadata"]["HTTPStatusCode"].should.equal(409)
@mock_databrew
def test_publish_recipe():
client = _create_databrew_client()
response = _create_test_recipe(client)
recipe_name = response["Name"]
# Before a recipe is published, we should not be able to retrieve a published version
with pytest.raises(ClientError) as exc:
recipe = client.describe_recipe(Name=recipe_name)
err = exc.value.response["Error"]
err["Code"].should.equal("ResourceNotFoundException")
dt_before_publish = datetime.now().astimezone()
# Publish the recipe
publish_response = client.publish_recipe(Name=recipe_name, Description="1st desc")
publish_response["Name"].should.equal(recipe_name)
# Recipe is now published, so check we can retrieve the published version
recipe = client.describe_recipe(Name=recipe_name)
recipe["Description"].should.equal("1st desc")
recipe["RecipeVersion"].should.equal("1.0")
recipe["PublishedDate"].should.be.greater_than(dt_before_publish)
first_published_date = recipe["PublishedDate"]
# Publish the recipe a 2nd time
publish_response = client.publish_recipe(Name=recipe_name, Description="2nd desc")
publish_response["Name"].should.equal(recipe_name)
recipe = client.describe_recipe(Name=recipe_name)
recipe["Description"].should.equal("2nd desc")
recipe["RecipeVersion"].should.equal("2.0")
recipe["PublishedDate"].should.be.greater_than(first_published_date)
@mock_databrew
def test_publish_recipe_that_does_not_exist():
client = _create_databrew_client()
with pytest.raises(ClientError) as exc:
client.publish_recipe(Name="DoesNotExist")
err = exc.value.response["Error"]
err["Code"].should.equal("ResourceNotFoundException")
exc.value.response["ResponseMetadata"]["HTTPStatusCode"].should.equal(404)
@mock_databrew
def test_publish_long_recipe_name():
client = _create_databrew_client()
name = "a" * 256
with pytest.raises(ClientError) as exc:
client.publish_recipe(Name=name)
err = exc.value.response["Error"]
err["Code"].should.equal("ValidationException")
err["Message"].should.equal(
f"1 validation error detected: Value '{name}' at 'name' failed to satisfy constraint: "
f"Member must have length less than or equal to 255"
)
exc.value.response["ResponseMetadata"]["HTTPStatusCode"].should.equal(400)
err = exc.value.response["Error"]
err["Code"].should.equal("ValidationException")
@mock_databrew
def test_delete_recipe_version():
client = _create_databrew_client()
response = _create_test_recipe(client)
recipe_name = response["Name"]
client.delete_recipe_version(Name=recipe_name, RecipeVersion="LATEST_WORKING")
with pytest.raises(ClientError) as exc:
client.describe_recipe(Name=recipe_name)
err = exc.value.response["Error"]
err["Code"].should.equal("ResourceNotFoundException")
exc.value.response["ResponseMetadata"]["HTTPStatusCode"].should.equal(404)
@mock_databrew
def test_delete_recipe_version_published():
client = _create_databrew_client()
response = _create_test_recipe(client)
recipe_name = response["Name"]
client.publish_recipe(Name=recipe_name)
client.delete_recipe_version(Name=recipe_name, RecipeVersion="1.0")
with pytest.raises(ClientError) as exc:
client.describe_recipe(Name=recipe_name)
err = exc.value.response["Error"]
err["Code"].should.equal("ResourceNotFoundException")
exc.value.response["ResponseMetadata"]["HTTPStatusCode"].should.equal(404)
recipe = client.describe_recipe(Name=recipe_name, RecipeVersion="1.1")
recipe["RecipeVersion"].should.equal("1.1")
@mock_databrew
def test_delete_recipe_version_latest_working_after_publish():
client = _create_databrew_client()
response = _create_test_recipe(client)
recipe_name = response["Name"]
client.publish_recipe(Name=recipe_name)
with pytest.raises(ClientError) as exc:
client.delete_recipe_version(Name=recipe_name, RecipeVersion="LATEST_WORKING")
err = exc.value.response["Error"]
err["Code"].should.equal("ValidationException")
err["Message"].should.equal(
"Recipe version LATEST_WORKING is not allowed to be deleted"
)
exc.value.response["ResponseMetadata"]["HTTPStatusCode"].should.equal(400)
@mock_databrew
def test_delete_recipe_version_latest_working_numeric_after_publish():
client = _create_databrew_client()
response = _create_test_recipe(client)
recipe_name = response["Name"]
client.publish_recipe(Name=recipe_name)
with pytest.raises(ClientError) as exc:
client.delete_recipe_version(Name=recipe_name, RecipeVersion="1.1")
err = exc.value.response["Error"]
err["Code"].should.equal("ValidationException")
err["Message"].should.equal("Recipe version 1.1 is not allowed to be deleted")
exc.value.response["ResponseMetadata"]["HTTPStatusCode"].should.equal(400)
@mock_databrew
def test_delete_recipe_version_invalid_version_string():
client = _create_databrew_client()
response = _create_test_recipe(client)
recipe_name = response["Name"]
recipe_version = "NotValid"
client.publish_recipe(Name=recipe_name)
with pytest.raises(ClientError) as exc:
client.delete_recipe_version(Name=recipe_name, RecipeVersion=recipe_version)
err = exc.value.response["Error"]
err["Code"].should.equal("ValidationException")
err["Message"].should.equal(
f"Recipe {recipe_name} version {recipe_version} is invalid."
)
exc.value.response["ResponseMetadata"]["HTTPStatusCode"].should.equal(400)
@mock_databrew
def test_delete_recipe_version_invalid_version_length():
client = _create_databrew_client()
response = _create_test_recipe(client)
recipe_name = response["Name"]
recipe_version = "1" * 17
client.publish_recipe(Name=recipe_name)
with pytest.raises(ClientError) as exc:
client.delete_recipe_version(Name=recipe_name, RecipeVersion=recipe_version)
err = exc.value.response["Error"]
err["Code"].should.equal("ValidationException")
err["Message"].should.equal(
f"Recipe {recipe_name} version {recipe_version} is invalid."
)
exc.value.response["ResponseMetadata"]["HTTPStatusCode"].should.equal(400)
@mock_databrew
def test_delete_recipe_version_unknown_recipe():
client = _create_databrew_client()
recipe_name = "Unknown"
with pytest.raises(ClientError) as exc:
client.delete_recipe_version(Name=recipe_name, RecipeVersion="1.1")
err = exc.value.response["Error"]
err["Code"].should.equal("ResourceNotFoundException")
err["Message"].should.equal(f"The recipe {recipe_name} wasn't found")
exc.value.response["ResponseMetadata"]["HTTPStatusCode"].should.equal(404)
@mock_databrew
def test_delete_recipe_version_unknown_version():
client = _create_databrew_client()
response = _create_test_recipe(client)
recipe_name = response["Name"]
recipe_version = "1.1"
with pytest.raises(ClientError) as exc:
client.delete_recipe_version(Name=recipe_name, RecipeVersion=recipe_version)
err = exc.value.response["Error"]
err["Code"].should.equal("ResourceNotFoundException")
err["Message"].should.equal(
f"The recipe {recipe_name} version {recipe_version} wasn't found."
)
exc.value.response["ResponseMetadata"]["HTTPStatusCode"].should.equal(404)