Adds some basic endpoints for Amazon Forecast (#3434)
* Adding some basic endpoints for Amazon Forecast, including all dataset group related endpoints * Adds better testing around exception handling in forecast endpoint, removes some unused code, and cleans up validation code * Fix unused imports, optimize imports, code style fixes Co-authored-by: Paul Miller <pwmiller@amazon.com>
This commit is contained in:
parent
b7cf2d4478
commit
725ad7571d
12 changed files with 555 additions and 6 deletions
7
moto/forecast/__init__.py
Normal file
7
moto/forecast/__init__.py
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
from __future__ import unicode_literals
|
||||
|
||||
from .models import forecast_backends
|
||||
from ..core.models import base_decorator
|
||||
|
||||
forecast_backend = forecast_backends["us-east-1"]
|
||||
mock_forecast = base_decorator(forecast_backends)
|
||||
43
moto/forecast/exceptions.py
Normal file
43
moto/forecast/exceptions.py
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
from __future__ import unicode_literals
|
||||
|
||||
import json
|
||||
|
||||
|
||||
class AWSError(Exception):
|
||||
TYPE = None
|
||||
STATUS = 400
|
||||
|
||||
def __init__(self, message, type=None, status=None):
|
||||
self.message = message
|
||||
self.type = type if type is not None else self.TYPE
|
||||
self.status = status if status is not None else self.STATUS
|
||||
|
||||
def response(self):
|
||||
return (
|
||||
json.dumps({"__type": self.type, "message": self.message}),
|
||||
dict(status=self.status),
|
||||
)
|
||||
|
||||
|
||||
class InvalidInputException(AWSError):
|
||||
TYPE = "InvalidInputException"
|
||||
|
||||
|
||||
class ResourceAlreadyExistsException(AWSError):
|
||||
TYPE = "ResourceAlreadyExistsException"
|
||||
|
||||
|
||||
class ResourceNotFoundException(AWSError):
|
||||
TYPE = "ResourceNotFoundException"
|
||||
|
||||
|
||||
class ResourceInUseException(AWSError):
|
||||
TYPE = "ResourceInUseException"
|
||||
|
||||
|
||||
class LimitExceededException(AWSError):
|
||||
TYPE = "LimitExceededException"
|
||||
|
||||
|
||||
class ValidationException(AWSError):
|
||||
TYPE = "ValidationException"
|
||||
173
moto/forecast/models.py
Normal file
173
moto/forecast/models.py
Normal file
|
|
@ -0,0 +1,173 @@
|
|||
import re
|
||||
from datetime import datetime
|
||||
|
||||
from boto3 import Session
|
||||
from future.utils import iteritems
|
||||
|
||||
from moto.core import ACCOUNT_ID, BaseBackend
|
||||
from moto.core.utils import iso_8601_datetime_without_milliseconds
|
||||
from .exceptions import (
|
||||
InvalidInputException,
|
||||
ResourceAlreadyExistsException,
|
||||
ResourceNotFoundException,
|
||||
ValidationException,
|
||||
)
|
||||
|
||||
|
||||
class DatasetGroup:
|
||||
accepted_dataset_group_name_format = re.compile(r"^[a-zA-Z][a-z-A-Z0-9_]*")
|
||||
accepted_dataset_group_arn_format = re.compile(r"^[a-zA-Z0-9\-\_\.\/\:]+$")
|
||||
accepted_dataset_types = [
|
||||
"INVENTORY_PLANNING",
|
||||
"METRICS",
|
||||
"RETAIL",
|
||||
"EC2_CAPACITY",
|
||||
"CUSTOM",
|
||||
"WEB_TRAFFIC",
|
||||
"WORK_FORCE",
|
||||
]
|
||||
|
||||
def __init__(
|
||||
self, region_name, dataset_arns, dataset_group_name, domain, tags=None
|
||||
):
|
||||
self.creation_date = iso_8601_datetime_without_milliseconds(datetime.now())
|
||||
self.modified_date = self.creation_date
|
||||
|
||||
self.arn = (
|
||||
"arn:aws:forecast:"
|
||||
+ region_name
|
||||
+ ":"
|
||||
+ str(ACCOUNT_ID)
|
||||
+ ":dataset-group/"
|
||||
+ dataset_group_name
|
||||
)
|
||||
self.dataset_arns = dataset_arns if dataset_arns else []
|
||||
self.dataset_group_name = dataset_group_name
|
||||
self.domain = domain
|
||||
self.tags = tags
|
||||
self._validate()
|
||||
|
||||
def update(self, dataset_arns):
|
||||
self.dataset_arns = dataset_arns
|
||||
self.last_modified_date = iso_8601_datetime_without_milliseconds(datetime.now())
|
||||
|
||||
def _validate(self):
|
||||
errors = []
|
||||
|
||||
errors.extend(self._validate_dataset_group_name())
|
||||
errors.extend(self._validate_dataset_group_name_len())
|
||||
errors.extend(self._validate_dataset_group_domain())
|
||||
|
||||
if errors:
|
||||
err_count = len(errors)
|
||||
message = str(err_count) + " validation error"
|
||||
message += "s" if err_count > 1 else ""
|
||||
message += " detected: "
|
||||
message += "; ".join(errors)
|
||||
raise ValidationException(message)
|
||||
|
||||
def _validate_dataset_group_name(self):
|
||||
errors = []
|
||||
if not re.match(
|
||||
self.accepted_dataset_group_name_format, self.dataset_group_name
|
||||
):
|
||||
errors.append(
|
||||
"Value '"
|
||||
+ self.dataset_group_name
|
||||
+ "' at 'datasetGroupName' failed to satisfy constraint: Member must satisfy regular expression pattern "
|
||||
+ self.accepted_dataset_group_name_format.pattern
|
||||
)
|
||||
return errors
|
||||
|
||||
def _validate_dataset_group_name_len(self):
|
||||
errors = []
|
||||
if len(self.dataset_group_name) >= 64:
|
||||
errors.append(
|
||||
"Value '"
|
||||
+ self.dataset_group_name
|
||||
+ "' at 'datasetGroupName' failed to satisfy constraint: Member must have length less than or equal to 63"
|
||||
)
|
||||
return errors
|
||||
|
||||
def _validate_dataset_group_domain(self):
|
||||
errors = []
|
||||
if self.domain not in self.accepted_dataset_types:
|
||||
errors.append(
|
||||
"Value '"
|
||||
+ self.domain
|
||||
+ "' at 'domain' failed to satisfy constraint: Member must satisfy enum value set "
|
||||
+ str(self.accepted_dataset_types)
|
||||
)
|
||||
return errors
|
||||
|
||||
|
||||
class ForecastBackend(BaseBackend):
|
||||
def __init__(self, region_name):
|
||||
super(ForecastBackend, self).__init__()
|
||||
self.dataset_groups = {}
|
||||
self.datasets = {}
|
||||
self.region_name = region_name
|
||||
|
||||
def create_dataset_group(self, dataset_group_name, domain, dataset_arns, tags):
|
||||
dataset_group = DatasetGroup(
|
||||
region_name=self.region_name,
|
||||
dataset_group_name=dataset_group_name,
|
||||
domain=domain,
|
||||
dataset_arns=dataset_arns,
|
||||
tags=tags,
|
||||
)
|
||||
|
||||
if dataset_arns:
|
||||
for dataset_arn in dataset_arns:
|
||||
if dataset_arn not in self.datasets:
|
||||
raise InvalidInputException(
|
||||
"Dataset arns: [" + dataset_arn + "] are not found"
|
||||
)
|
||||
|
||||
if self.dataset_groups.get(dataset_group.arn):
|
||||
raise ResourceAlreadyExistsException(
|
||||
"A dataset group already exists with the arn: " + dataset_group.arn
|
||||
)
|
||||
|
||||
self.dataset_groups[dataset_group.arn] = dataset_group
|
||||
return dataset_group
|
||||
|
||||
def describe_dataset_group(self, dataset_group_arn):
|
||||
try:
|
||||
dataset_group = self.dataset_groups[dataset_group_arn]
|
||||
except KeyError:
|
||||
raise ResourceNotFoundException("No resource found " + dataset_group_arn)
|
||||
return dataset_group
|
||||
|
||||
def delete_dataset_group(self, dataset_group_arn):
|
||||
try:
|
||||
del self.dataset_groups[dataset_group_arn]
|
||||
except KeyError:
|
||||
raise ResourceNotFoundException("No resource found " + dataset_group_arn)
|
||||
|
||||
def update_dataset_group(self, dataset_group_arn, dataset_arns):
|
||||
try:
|
||||
dsg = self.dataset_groups[dataset_group_arn]
|
||||
except KeyError:
|
||||
raise ResourceNotFoundException("No resource found " + dataset_group_arn)
|
||||
|
||||
for dataset_arn in dataset_arns:
|
||||
if dataset_arn not in dsg.dataset_arns:
|
||||
raise InvalidInputException(
|
||||
"Dataset arns: [" + dataset_arn + "] are not found"
|
||||
)
|
||||
|
||||
dsg.update(dataset_arns)
|
||||
|
||||
def list_dataset_groups(self):
|
||||
return [v for (_, v) in iteritems(self.dataset_groups)]
|
||||
|
||||
def reset(self):
|
||||
region_name = self.region_name
|
||||
self.__dict__ = {}
|
||||
self.__init__(region_name)
|
||||
|
||||
|
||||
forecast_backends = {}
|
||||
for region in Session().get_available_regions("forecast"):
|
||||
forecast_backends[region] = ForecastBackend(region)
|
||||
92
moto/forecast/responses.py
Normal file
92
moto/forecast/responses.py
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
from __future__ import unicode_literals
|
||||
|
||||
import json
|
||||
|
||||
from moto.core.responses import BaseResponse
|
||||
from moto.core.utils import amzn_request_id
|
||||
from .exceptions import AWSError
|
||||
from .models import forecast_backends
|
||||
|
||||
|
||||
class ForecastResponse(BaseResponse):
|
||||
@property
|
||||
def forecast_backend(self):
|
||||
return forecast_backends[self.region]
|
||||
|
||||
@amzn_request_id
|
||||
def create_dataset_group(self):
|
||||
dataset_group_name = self._get_param("DatasetGroupName")
|
||||
domain = self._get_param("Domain")
|
||||
dataset_arns = self._get_param("DatasetArns")
|
||||
tags = self._get_param("Tags")
|
||||
|
||||
try:
|
||||
dataset_group = self.forecast_backend.create_dataset_group(
|
||||
dataset_group_name=dataset_group_name,
|
||||
domain=domain,
|
||||
dataset_arns=dataset_arns,
|
||||
tags=tags,
|
||||
)
|
||||
response = {"DatasetGroupArn": dataset_group.arn}
|
||||
return 200, {}, json.dumps(response)
|
||||
except AWSError as err:
|
||||
return err.response()
|
||||
|
||||
@amzn_request_id
|
||||
def describe_dataset_group(self):
|
||||
dataset_group_arn = self._get_param("DatasetGroupArn")
|
||||
|
||||
try:
|
||||
dataset_group = self.forecast_backend.describe_dataset_group(
|
||||
dataset_group_arn=dataset_group_arn
|
||||
)
|
||||
response = {
|
||||
"CreationTime": dataset_group.creation_date,
|
||||
"DatasetArns": dataset_group.dataset_arns,
|
||||
"DatasetGroupArn": dataset_group.arn,
|
||||
"DatasetGroupName": dataset_group.dataset_group_name,
|
||||
"Domain": dataset_group.domain,
|
||||
"LastModificationTime": dataset_group.modified_date,
|
||||
"Status": "ACTIVE",
|
||||
}
|
||||
return 200, {}, json.dumps(response)
|
||||
except AWSError as err:
|
||||
return err.response()
|
||||
|
||||
@amzn_request_id
|
||||
def delete_dataset_group(self):
|
||||
dataset_group_arn = self._get_param("DatasetGroupArn")
|
||||
try:
|
||||
self.forecast_backend.delete_dataset_group(dataset_group_arn)
|
||||
return 200, {}, None
|
||||
except AWSError as err:
|
||||
return err.response()
|
||||
|
||||
@amzn_request_id
|
||||
def update_dataset_group(self):
|
||||
dataset_group_arn = self._get_param("DatasetGroupArn")
|
||||
dataset_arns = self._get_param("DatasetArns")
|
||||
try:
|
||||
self.forecast_backend.update_dataset_group(dataset_group_arn, dataset_arns)
|
||||
return 200, {}, None
|
||||
except AWSError as err:
|
||||
return err.response()
|
||||
|
||||
@amzn_request_id
|
||||
def list_dataset_groups(self):
|
||||
list_all = self.forecast_backend.list_dataset_groups()
|
||||
list_all = sorted(
|
||||
[
|
||||
{
|
||||
"DatasetGroupArn": dsg.arn,
|
||||
"DatasetGroupName": dsg.dataset_group_name,
|
||||
"CreationTime": dsg.creation_date,
|
||||
"LastModificationTime": dsg.creation_date,
|
||||
}
|
||||
for dsg in list_all
|
||||
],
|
||||
key=lambda x: x["LastModificationTime"],
|
||||
reverse=True,
|
||||
)
|
||||
response = {"DatasetGroups": list_all}
|
||||
return 200, {}, json.dumps(response)
|
||||
7
moto/forecast/urls.py
Normal file
7
moto/forecast/urls.py
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
from __future__ import unicode_literals
|
||||
|
||||
from .responses import ForecastResponse
|
||||
|
||||
url_bases = ["https?://forecast.(.+).amazonaws.com"]
|
||||
|
||||
url_paths = {"{0}/$": ForecastResponse.dispatch}
|
||||
Loading…
Add table
Add a link
Reference in a new issue