This adds a new service "databrew", mostly around managing databrew (#4898)

This commit is contained in:
John-Paul Stanford 2022-03-07 20:53:45 +00:00 committed by GitHub
parent 7001ec60df
commit a460adc940
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 300 additions and 1 deletions

View File

@ -49,6 +49,7 @@ mock_cognitoidentity = lazy_load(
)
mock_cognitoidp = lazy_load(".cognitoidp", "mock_cognitoidp", boto3_name="cognito-idp")
mock_config = lazy_load(".config", "mock_config")
mock_databrew = lazy_load(".databrew", "mock_databrew")
mock_datapipeline = lazy_load(".datapipeline", "mock_datapipeline")
mock_datasync = lazy_load(".datasync", "mock_datasync")
mock_dax = lazy_load(".dax", "mock_dax")

View File

@ -1,4 +1,4 @@
# autogenerated by scripts/update_backend_index.py
# autogenerated by ./scripts/update_backend_index.py
import re
backend_url_patterns = [
@ -25,6 +25,7 @@ backend_url_patterns = [
),
("cognito-idp", re.compile("https?://cognito-idp\\.(.+)\\.amazonaws.com")),
("config", re.compile("https?://config\\.(.+)\\.amazonaws\\.com")),
("databrew", re.compile("https?://databrew\\.(.+)\\.amazonaws.com")),
("datapipeline", re.compile("https?://datapipeline\\.(.+)\\.amazonaws\\.com")),
("datasync", re.compile("https?://(.*\\.)?(datasync)\\.(.+)\\.amazonaws.com")),
("dax", re.compile("https?://dax\\.(.+)\\.amazonaws\\.com")),

View File

@ -0,0 +1,4 @@
from .models import databrew_backends
from ..core.models import base_decorator
mock_databrew = base_decorator(databrew_backends)

View File

@ -0,0 +1,25 @@
from moto.core.exceptions import JsonRESTError
class DataBrewClientError(JsonRESTError):
code = 400
class AlreadyExistsException(DataBrewClientError):
def __init__(self, typ):
super().__init__("AlreadyExistsException", "%s already exists." % (typ))
class RecipeAlreadyExistsException(AlreadyExistsException):
def __init__(self):
super().__init__("Recipe")
class EntityNotFoundException(DataBrewClientError):
def __init__(self, msg):
super().__init__("EntityNotFoundException", msg)
class RecipeNotFoundException(EntityNotFoundException):
def __init__(self, recipe_name):
super().__init__("Recipe %s not found." % recipe_name)

71
moto/databrew/models.py Normal file
View File

@ -0,0 +1,71 @@
from collections import OrderedDict
from datetime import datetime
from moto.core import BaseBackend, BaseModel
from moto.core.utils import BackendDict
from moto.utilities.paginator import paginate
from .exceptions import RecipeAlreadyExistsException, RecipeNotFoundException
class DataBrewBackend(BaseBackend):
PAGINATION_MODEL = {
"list_recipes": {
"input_token": "next_token",
"limit_key": "max_results",
"limit_default": 100,
"unique_attribute": "name",
},
}
def __init__(self, region_name):
self.region_name = region_name
self.recipes = OrderedDict()
def reset(self):
"""Re-initialize all attributes for this instance."""
region_name = self.region_name
self.__init__(region_name)
def create_recipe(self, recipe_name, recipe_description, recipe_steps, tags):
# https://docs.aws.amazon.com/databrew/latest/dg/API_CreateRecipe.html
if recipe_name in self.recipes:
raise RecipeAlreadyExistsException()
recipe = FakeRecipe(
self.region_name, recipe_name, recipe_description, recipe_steps, tags
)
self.recipes[recipe_name] = recipe
return recipe
@paginate(pagination_model=PAGINATION_MODEL)
def list_recipes(self):
return [self.recipes[key] for key in self.recipes] if self.recipes else []
def get_recipe(self, recipe_name, version):
if recipe_name not in self.recipes:
raise RecipeNotFoundException(recipe_name)
return self.recipes[recipe_name]
class FakeRecipe(BaseModel):
def __init__(
self, region_name, recipe_name, recipe_description, recipe_steps, tags
):
self.region_name = region_name
self.name = recipe_name
self.description = recipe_description
self.steps = recipe_steps
self.created_time = datetime.now()
self.tags = tags
def as_dict(self):
return {
"Name": self.name,
"Steps": self.steps,
"Description": self.description,
"CreateTime": self.created_time.isoformat(),
"Tags": self.tags or dict(),
}
databrew_backends = BackendDict(DataBrewBackend, "databrew")

View File

@ -0,0 +1,69 @@
import json
from urllib.parse import urlparse
from moto.core.responses import BaseResponse
from moto.core.utils import amzn_request_id
from .exceptions import DataBrewClientError
from .models import databrew_backends
class DataBrewResponse(BaseResponse):
SERVICE_NAME = "databrew"
@property
def databrew_backend(self):
"""Return backend instance specific for this region."""
return databrew_backends[self.region]
@property
def parameters(self):
return json.loads(self.body)
@amzn_request_id
def create_recipe(self):
# https://docs.aws.amazon.com/databrew/latest/dg/API_CreateRecipe.html
recipe_description = self.parameters.get("Description")
recipe_steps = self.parameters.get("Steps")
recipe_name = self.parameters.get("Name")
tags = self.parameters.get("Tags")
return json.dumps(
self.databrew_backend.create_recipe(
recipe_name, recipe_description, recipe_steps, tags
).as_dict()
)
@amzn_request_id
def list_recipes(self):
# https://docs.aws.amazon.com/databrew/latest/dg/API_ListRecipes.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
recipe_list, next_token = self.databrew_backend.list_recipes(
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 describe_recipe_response(self, request, full_url, headers):
self.setup_class(request, full_url, headers)
parsed_url = urlparse(full_url)
recipe_name = parsed_url.path.rstrip("/").rsplit("/", 1)[1]
try:
return json.dumps(
self.databrew_backend.get_recipe(recipe_name, None).as_dict()
)
except DataBrewClientError as e:
return e.code, e.get_headers(), e.get_body()
# 'DescribeRecipe'

8
moto/databrew/urls.py Normal file
View File

@ -0,0 +1,8 @@
from .responses import DataBrewResponse
url_bases = [r"https?://databrew\.(.+)\.amazonaws.com"]
url_paths = {
"{0}/recipes$": DataBrewResponse.dispatch,
"{0}/recipes/(?P<recipe_name>[^/]+)$": DataBrewResponse().describe_recipe_response,
}

View File

View File

@ -0,0 +1,120 @@
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_recipe(client, tags=None, recipe_name=None):
if recipe_name is None:
recipe_name = str(uuid.uuid4())
return client.create_recipe(
Name=recipe_name,
Steps=[
{
"Action": {
"Operation": "REMOVE_COMBINED",
"Parameters": {
"collapseConsecutiveWhitespace": "false",
"removeAllPunctuation": "false",
"removeAllQuotes": "false",
"removeAllWhitespace": "false",
"removeCustomCharacters": "false",
"removeCustomValue": "false",
"removeLeadingAndTrailingPunctuation": "false",
"removeLeadingAndTrailingQuotes": "false",
"removeLeadingAndTrailingWhitespace": "false",
"removeLetters": "false",
"removeNumbers": "false",
"removeSpecialCharacters": "true",
"sourceColumn": "FakeColumn",
},
}
}
],
Tags=tags or {},
)
def _create_test_recipes(client, count):
for _ in range(count):
_create_test_recipe(client)
@mock_databrew
def test_recipe_list_when_empty():
client = _create_databrew_client()
response = client.list_recipes()
response.should.have.key("Recipes")
response["Recipes"].should.have.length_of(0)
@mock_databrew
def test_list_recipes_with_max_results():
client = _create_databrew_client()
_create_test_recipes(client, 4)
response = client.list_recipes(MaxResults=2)
response["Recipes"].should.have.length_of(2)
response.should.have.key("NextToken")
@mock_databrew
def test_list_recipes_from_next_token():
client = _create_databrew_client()
_create_test_recipes(client, 10)
first_response = client.list_recipes(MaxResults=3)
response = client.list_recipes(NextToken=first_response["NextToken"])
response["Recipes"].should.have.length_of(7)
@mock_databrew
def test_list_recipes_with_max_results_greater_than_actual_results():
client = _create_databrew_client()
_create_test_recipes(client, 4)
response = client.list_recipes(MaxResults=10)
response["Recipes"].should.have.length_of(4)
@mock_databrew
def test_describe_recipe():
client = _create_databrew_client()
response = _create_test_recipe(client)
recipe = client.describe_recipe(Name=response["Name"])
recipe["Name"].should.equal(response["Name"])
recipe["Steps"].should.have.length_of(1)
@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("EntityNotFoundException")
err["Message"].should.equal("Recipe DoseNotExist not found.")
@mock_databrew
def test_create_recipe_that_already_exists():
client = _create_databrew_client()
response = _create_test_recipe(client)
with pytest.raises(ClientError) as exc:
_create_test_recipe(client, recipe_name=response["Name"])
err = exc.value.response["Error"]
err["Code"].should.equal("AlreadyExistsException")
err["Message"].should.equal("Recipe already exists.")