diff --git a/IMPLEMENTATION_COVERAGE.md b/IMPLEMENTATION_COVERAGE.md index b1a53f753..dec6250a6 100644 --- a/IMPLEMENTATION_COVERAGE.md +++ b/IMPLEMENTATION_COVERAGE.md @@ -4199,6 +4199,72 @@ - [X] update_policy +## personalize +
+6% implemented + +- [ ] create_batch_inference_job +- [ ] create_batch_segment_job +- [ ] create_campaign +- [ ] create_dataset +- [ ] create_dataset_export_job +- [ ] create_dataset_group +- [ ] create_dataset_import_job +- [ ] create_event_tracker +- [ ] create_filter +- [ ] create_recommender +- [X] create_schema +- [ ] create_solution +- [ ] create_solution_version +- [ ] delete_campaign +- [ ] delete_dataset +- [ ] delete_dataset_group +- [ ] delete_event_tracker +- [ ] delete_filter +- [ ] delete_recommender +- [X] delete_schema +- [ ] delete_solution +- [ ] describe_algorithm +- [ ] describe_batch_inference_job +- [ ] describe_batch_segment_job +- [ ] describe_campaign +- [ ] describe_dataset +- [ ] describe_dataset_export_job +- [ ] describe_dataset_group +- [ ] describe_dataset_import_job +- [ ] describe_event_tracker +- [ ] describe_feature_transformation +- [ ] describe_filter +- [ ] describe_recipe +- [ ] describe_recommender +- [X] describe_schema +- [ ] describe_solution +- [ ] describe_solution_version +- [ ] get_solution_metrics +- [ ] list_batch_inference_jobs +- [ ] list_batch_segment_jobs +- [ ] list_campaigns +- [ ] list_dataset_export_jobs +- [ ] list_dataset_groups +- [ ] list_dataset_import_jobs +- [ ] list_datasets +- [ ] list_event_trackers +- [ ] list_filters +- [ ] list_recipes +- [ ] list_recommenders +- [X] list_schemas +- [ ] list_solution_versions +- [ ] list_solutions +- [ ] list_tags_for_resource +- [ ] start_recommender +- [ ] stop_recommender +- [ ] stop_solution_version_creation +- [ ] tag_resource +- [ ] untag_resource +- [ ] update_campaign +- [ ] update_recommender +
+ ## pinpoint
10% implemented @@ -6268,7 +6334,6 @@ - opsworkscm - outposts - panorama -- personalize - personalize-events - personalize-runtime - pi diff --git a/docs/docs/services/personalize.rst b/docs/docs/services/personalize.rst new file mode 100644 index 000000000..dd615ace6 --- /dev/null +++ b/docs/docs/services/personalize.rst @@ -0,0 +1,94 @@ +.. _implementedservice_personalize: + +.. |start-h3| raw:: html + +

+ +.. |end-h3| raw:: html + +

+ +=========== +personalize +=========== + +.. autoclass:: moto.personalize.models.PersonalizeBackend + +|start-h3| Example usage |end-h3| + +.. sourcecode:: python + + @mock_personalize + def test_personalize_behaviour: + boto3.client("personalize") + ... + + + +|start-h3| Implemented features for this service |end-h3| + +- [ ] create_batch_inference_job +- [ ] create_batch_segment_job +- [ ] create_campaign +- [ ] create_dataset +- [ ] create_dataset_export_job +- [ ] create_dataset_group +- [ ] create_dataset_import_job +- [ ] create_event_tracker +- [ ] create_filter +- [ ] create_recommender +- [X] create_schema +- [ ] create_solution +- [ ] create_solution_version +- [ ] delete_campaign +- [ ] delete_dataset +- [ ] delete_dataset_group +- [ ] delete_event_tracker +- [ ] delete_filter +- [ ] delete_recommender +- [X] delete_schema +- [ ] delete_solution +- [ ] describe_algorithm +- [ ] describe_batch_inference_job +- [ ] describe_batch_segment_job +- [ ] describe_campaign +- [ ] describe_dataset +- [ ] describe_dataset_export_job +- [ ] describe_dataset_group +- [ ] describe_dataset_import_job +- [ ] describe_event_tracker +- [ ] describe_feature_transformation +- [ ] describe_filter +- [ ] describe_recipe +- [ ] describe_recommender +- [X] describe_schema +- [ ] describe_solution +- [ ] describe_solution_version +- [ ] get_solution_metrics +- [ ] list_batch_inference_jobs +- [ ] list_batch_segment_jobs +- [ ] list_campaigns +- [ ] list_dataset_export_jobs +- [ ] list_dataset_groups +- [ ] list_dataset_import_jobs +- [ ] list_datasets +- [ ] list_event_trackers +- [ ] list_filters +- [ ] list_recipes +- [ ] list_recommenders +- [X] list_schemas + + Pagination is not yet implemented + + +- [ ] list_solution_versions +- [ ] list_solutions +- [ ] list_tags_for_resource +- [ ] start_recommender +- [ ] stop_recommender +- [ ] stop_solution_version_creation +- [ ] tag_resource +- [ ] untag_resource +- [ ] update_campaign +- [ ] update_recommender + diff --git a/moto/__init__.py b/moto/__init__.py index 19dfaf7b2..c99b6f3b6 100644 --- a/moto/__init__.py +++ b/moto/__init__.py @@ -107,6 +107,7 @@ mock_mediastoredata = lazy_load( mock_mq = lazy_load(".mq", "mock_mq", boto3_name="mq") mock_opsworks = lazy_load(".opsworks", "mock_opsworks") mock_organizations = lazy_load(".organizations", "mock_organizations") +mock_personalize = lazy_load(".personalize", "mock_personalize") mock_pinpoint = lazy_load(".pinpoint", "mock_pinpoint") mock_polly = lazy_load(".polly", "mock_polly") mock_quicksight = lazy_load(".quicksight", "mock_quicksight") diff --git a/moto/backend_index.py b/moto/backend_index.py index 4ff8408f4..975d24440 100644 --- a/moto/backend_index.py +++ b/moto/backend_index.py @@ -103,6 +103,7 @@ backend_url_patterns = [ ("mq", re.compile("https?://mq\\.(.+)\\.amazonaws\\.com")), ("opsworks", re.compile("https?://opsworks\\.us-east-1\\.amazonaws.com")), ("organizations", re.compile("https?://organizations\\.(.+)\\.amazonaws\\.com")), + ("personalize", re.compile("https?://personalize\\.(.+)\\.amazonaws\\.com")), ("pinpoint", re.compile("https?://pinpoint\\.(.+)\\.amazonaws\\.com")), ("polly", re.compile("https?://polly\\.(.+)\\.amazonaws.com")), ("quicksight", re.compile("https?://quicksight\\.(.+)\\.amazonaws\\.com")), diff --git a/moto/personalize/__init__.py b/moto/personalize/__init__.py new file mode 100644 index 000000000..fe2fdaa2a --- /dev/null +++ b/moto/personalize/__init__.py @@ -0,0 +1,5 @@ +"""personalize module initialization; sets value for base decorator.""" +from .models import personalize_backends +from ..core.models import base_decorator + +mock_personalize = base_decorator(personalize_backends) diff --git a/moto/personalize/exceptions.py b/moto/personalize/exceptions.py new file mode 100644 index 000000000..68259f691 --- /dev/null +++ b/moto/personalize/exceptions.py @@ -0,0 +1,13 @@ +"""Exceptions raised by the personalize service.""" +from moto.core.exceptions import JsonRESTError + + +class PersonalizeException(JsonRESTError): + pass + + +class ResourceNotFoundException(PersonalizeException): + def __init__(self, arn): + super().__init__( + "ResourceNotFoundException", f"Resource Arn {arn} does not exist." + ) diff --git a/moto/personalize/models.py b/moto/personalize/models.py new file mode 100644 index 000000000..6a44257cf --- /dev/null +++ b/moto/personalize/models.py @@ -0,0 +1,64 @@ +"""PersonalizeBackend class with methods for supported APIs.""" + +from .exceptions import ResourceNotFoundException +from moto.core import BaseBackend, BaseModel +from moto.core.utils import BackendDict, unix_time + + +class Schema(BaseModel): + def __init__(self, account_id, region, name, schema, domain): + self.name = name + self.schema = schema + self.domain = domain + self.arn = f"arn:aws:personalize:{region}:{account_id}:schema/{name}" + self.created = unix_time() + + def to_dict(self, full=True): + d = { + "name": self.name, + "schemaArn": self.arn, + "domain": self.domain, + "creationDateTime": self.created, + "lastUpdatedDateTime": self.created, + } + if full: + d["schema"] = self.schema + return d + + +class PersonalizeBackend(BaseBackend): + """Implementation of Personalize APIs.""" + + def __init__(self, region_name, account_id): + super().__init__(region_name, account_id) + self.schemas: [str, Schema] = dict() + + def create_schema(self, name, schema, domain): + schema = Schema( + region=self.region_name, + account_id=self.account_id, + name=name, + schema=schema, + domain=domain, + ) + self.schemas[schema.arn] = schema + return schema.arn + + def delete_schema(self, schema_arn): + if schema_arn not in self.schemas: + raise ResourceNotFoundException(schema_arn) + self.schemas.pop(schema_arn, None) + + def describe_schema(self, schema_arn): + if schema_arn not in self.schemas: + raise ResourceNotFoundException(schema_arn) + return self.schemas[schema_arn] + + def list_schemas(self) -> [Schema]: + """ + Pagination is not yet implemented + """ + return self.schemas.values() + + +personalize_backends = BackendDict(PersonalizeBackend, "personalize") diff --git a/moto/personalize/responses.py b/moto/personalize/responses.py new file mode 100644 index 000000000..ac100b799 --- /dev/null +++ b/moto/personalize/responses.py @@ -0,0 +1,48 @@ +"""Handles incoming personalize requests, invokes methods, returns responses.""" +import json + +from moto.core.responses import BaseResponse +from .models import personalize_backends + + +class PersonalizeResponse(BaseResponse): + """Handler for Personalize requests and responses.""" + + def __init__(self): + super().__init__(service_name="personalize") + + @property + def personalize_backend(self): + """Return backend instance specific for this region.""" + return personalize_backends[self.current_account][self.region] + + # add methods from here + + def create_schema(self): + params = json.loads(self.body) + name = params.get("name") + schema = params.get("schema") + domain = params.get("domain") + schema_arn = self.personalize_backend.create_schema( + name=name, + schema=schema, + domain=domain, + ) + return json.dumps(dict(schemaArn=schema_arn)) + + def delete_schema(self): + params = json.loads(self.body) + schema_arn = params.get("schemaArn") + self.personalize_backend.delete_schema(schema_arn=schema_arn) + return "{}" + + def describe_schema(self): + params = json.loads(self.body) + schema_arn = params.get("schemaArn") + schema = self.personalize_backend.describe_schema(schema_arn=schema_arn) + return json.dumps(dict(schema=schema.to_dict())) + + def list_schemas(self): + schemas = self.personalize_backend.list_schemas() + resp = {"schemas": [s.to_dict(full=False) for s in schemas]} + return json.dumps(resp) diff --git a/moto/personalize/urls.py b/moto/personalize/urls.py new file mode 100644 index 000000000..731bcdcab --- /dev/null +++ b/moto/personalize/urls.py @@ -0,0 +1,11 @@ +"""personalize base URL and path.""" +from .responses import PersonalizeResponse + +url_bases = [ + r"https?://personalize\.(.+)\.amazonaws\.com", +] + + +url_paths = { + "{0}/$": PersonalizeResponse.dispatch, +} diff --git a/tests/test_personalize/__init__.py b/tests/test_personalize/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/test_personalize/test_personalize_schema.py b/tests/test_personalize/test_personalize_schema.py new file mode 100644 index 000000000..29912e18f --- /dev/null +++ b/tests/test_personalize/test_personalize_schema.py @@ -0,0 +1,121 @@ +"""Unit tests for personalize-supported APIs.""" +import boto3 +import json +import sure # noqa # pylint: disable=unused-import +import pytest +from botocore.exceptions import ClientError +from moto import mock_personalize +from moto.core import DEFAULT_ACCOUNT_ID + +# See our Development Tips on writing tests for hints on how to write good tests: +# http://docs.getmoto.org/en/latest/docs/contributing/development_tips/tests.html + + +@mock_personalize +def test_create_schema(): + client = boto3.client("personalize", region_name="ap-southeast-1") + schema = { + "type": "record", + "name": "Interactions", + "namespace": "com.amazonaws.personalize.schema", + "fields": [ + {"name": "USER_ID", "type": "string"}, + {"name": "ITEM_ID", "type": "string"}, + {"name": "TIMESTAMP", "type": "long"}, + ], + "version": "1.0", + } + + create_schema_response = client.create_schema( + name="personalize-demo-schema", schema=json.dumps(schema) + ) + create_schema_response.should.have.key("schemaArn").equals( + f"arn:aws:personalize:ap-southeast-1:{DEFAULT_ACCOUNT_ID}:schema/personalize-demo-schema" + ) + + +@mock_personalize +def test_delete_schema(): + client = boto3.client("personalize", region_name="us-east-1") + schema_arn = client.create_schema(name="myname", schema=json.dumps("sth"))[ + "schemaArn" + ] + client.delete_schema(schemaArn=schema_arn) + + client.list_schemas().should.have.key("schemas").equals([]) + + +@mock_personalize +def test_delete_schema__unknown(): + arn = f"arn:aws:personalize:ap-southeast-1:{DEFAULT_ACCOUNT_ID}:schema/personalize-demo-schema" + client = boto3.client("personalize", region_name="us-east-2") + with pytest.raises(ClientError) as exc: + client.delete_schema(schemaArn=arn) + err = exc.value.response["Error"] + err["Code"].should.equal("ResourceNotFoundException") + err["Message"].should.equal(f"Resource Arn {arn} does not exist.") + + +@mock_personalize +def test_describe_schema(): + client = boto3.client("personalize", region_name="us-east-2") + schema_arn = client.create_schema(name="myname", schema="sth")["schemaArn"] + resp = client.describe_schema(schemaArn=schema_arn) + resp.should.have.key("schema") + schema = resp["schema"] + + schema.should.have.key("name").equals("myname") + schema.should.have.key("schemaArn").match("schema/myname") + schema.should.have.key("schema").equals("sth") + schema.should.have.key("creationDateTime") + schema.should.have.key("lastUpdatedDateTime") + + +@mock_personalize +def test_describe_schema__with_domain(): + client = boto3.client("personalize", region_name="us-east-2") + schema_arn = client.create_schema(name="myname", schema="sth", domain="ECOMMERCE")[ + "schemaArn" + ] + resp = client.describe_schema(schemaArn=schema_arn) + resp.should.have.key("schema") + schema = resp["schema"] + + schema.should.have.key("domain").equals("ECOMMERCE") + + +@mock_personalize +def test_describe_schema__unknown(): + arn = ( + "arn:aws:personalize:ap-southeast-1:486285699788:schema/personalize-demo-schema" + ) + client = boto3.client("personalize", region_name="us-east-2") + with pytest.raises(ClientError) as exc: + client.describe_schema(schemaArn=arn) + err = exc.value.response["Error"] + err["Code"].should.equal("ResourceNotFoundException") + err["Message"].should.equal(f"Resource Arn {arn} does not exist.") + + +@mock_personalize +def test_list_schemas__initial(): + client = boto3.client("personalize", region_name="us-east-2") + resp = client.list_schemas() + + resp.should.have.key("schemas").equals([]) + + +@mock_personalize +def test_list_schema(): + client = boto3.client("personalize", region_name="us-east-2") + schema_arn = client.create_schema(name="myname", schema="sth")["schemaArn"] + + resp = client.list_schemas() + resp.should.have.key("schemas").length_of(1) + schema = resp["schemas"][0] + + schema.should.have.key("name").equals("myname") + schema.should.have.key("schemaArn").equals(schema_arn) + schema.shouldnt.have.key("schema") + schema.should.have.key("creationDateTime") + schema.should.have.key("lastUpdatedDateTime")