From 4ed5a837dee4a4b9eb536b89aeefa50ca3a84c9f Mon Sep 17 00:00:00 2001 From: CoderDonohoe <1453680+AlanDonohoe@users.noreply.github.com> Date: Tue, 29 Mar 2022 14:15:25 +0100 Subject: [PATCH] Add Rekognition support. (#4957) --- moto/__init__.py | 3 + moto/backend_index.py | 1 + moto/rekognition/__init__.py | 5 + moto/rekognition/exceptions.py | 1 + moto/rekognition/models.py | 215 +++++++++++++++++++++ moto/rekognition/responses.py | 45 +++++ moto/rekognition/urls.py | 10 + tests/test_rekognition/__init__.py | 0 tests/test_rekognition/test_rekognition.py | 45 +++++ 9 files changed, 325 insertions(+) create mode 100644 moto/rekognition/__init__.py create mode 100644 moto/rekognition/exceptions.py create mode 100644 moto/rekognition/models.py create mode 100644 moto/rekognition/responses.py create mode 100644 moto/rekognition/urls.py create mode 100644 tests/test_rekognition/__init__.py create mode 100644 tests/test_rekognition/test_rekognition.py diff --git a/moto/__init__.py b/moto/__init__.py index 1b41bf6ac..92658a359 100644 --- a/moto/__init__.py +++ b/moto/__init__.py @@ -131,6 +131,9 @@ mock_redshift = lazy_load(".redshift", "mock_redshift") mock_redshiftdata = lazy_load( ".redshiftdata", "mock_redshiftdata", boto3_name="redshift-data" ) +mock_rekognition = lazy_load( + ".rekognition", "mock_rekognition", boto3_name="rekognition" +) mock_resourcegroups = lazy_load( ".resourcegroups", "mock_resourcegroups", boto3_name="resource-groups" ) diff --git a/moto/backend_index.py b/moto/backend_index.py index f75d767cd..167d960f5 100644 --- a/moto/backend_index.py +++ b/moto/backend_index.py @@ -102,6 +102,7 @@ backend_url_patterns = [ ("rds", re.compile("https?://rds\\.amazonaws\\.com")), ("redshift", re.compile("https?://redshift\\.(.+)\\.amazonaws\\.com")), ("redshift-data", re.compile("https?://redshift-data\\.(.+)\\.amazonaws\\.com")), + ("rekognition", re.compile("https?://rekognition\\.(.+)\\.amazonaws\\.com")), ( "resource-groups", re.compile("https?://resource-groups(-fips)?\\.(.+)\\.amazonaws.com"), diff --git a/moto/rekognition/__init__.py b/moto/rekognition/__init__.py new file mode 100644 index 000000000..cfa43786b --- /dev/null +++ b/moto/rekognition/__init__.py @@ -0,0 +1,5 @@ +"""rekognition module initialization; sets value for base decorator.""" +from .models import rekognition_backends +from ..core.models import base_decorator + +mock_rekognition = base_decorator(rekognition_backends) diff --git a/moto/rekognition/exceptions.py b/moto/rekognition/exceptions.py new file mode 100644 index 000000000..898e9321b --- /dev/null +++ b/moto/rekognition/exceptions.py @@ -0,0 +1 @@ +"""Exceptions raised by the rekognition service.""" diff --git a/moto/rekognition/models.py b/moto/rekognition/models.py new file mode 100644 index 000000000..4e9db42f3 --- /dev/null +++ b/moto/rekognition/models.py @@ -0,0 +1,215 @@ +"""RekognitionBackend class with methods for supported APIs.""" + +import random +import string + +from moto.core import BaseBackend +from moto.core.utils import BackendDict + + +class RekognitionBackend(BaseBackend): + """Implementation of Rekognition APIs.""" + + def __init__(self, region_name=None): + self.region_name = region_name + + def reset(self): + """Re-initialize all attributes for this instance.""" + region_name = self.region_name + self.__dict__ = {} + self.__init__(region_name) + + def start_text_detection(self): + return self._job_id() + + def get_text_detection(self): + """ + This returns hardcoded values and none of the parameters are taken into account. + """ + return ( + self._job_status(), + self._status_message(), + self._video_metadata(), + self._text_detections(), + self._next_token(), + self._text_model_version(), + ) + + # private + + def _job_id(self): + return "".join( + random.choice(string.ascii_uppercase + string.digits) for _ in range(64) + ) + + def _job_status(self): + return "SUCCEEDED" + + def _next_token(self): + return "" + + def _status_message(self): + return "" + + def _text_model_version(self): + return "3.1" + + def _video_metadata(self): + return { + "Codec": "h264", + "DurationMillis": 15020, + "Format": "QuickTime / MOV", + "FrameRate": 24.0, + "FrameHeight": 720, + "FrameWidth": 1280, + "ColorRange": "LIMITED", + } + + def _text_detections(self): + return [ + { + "Timestamp": 0, + "TextDetection": { + "DetectedText": "Hello world", + "Type": "LINE", + "Id": 0, + "Confidence": 97.89398956298828, + "Geometry": { + "BoundingBox": { + "Width": 0.1364741027355194, + "Height": 0.0318513885140419, + "Left": 0.4310702085494995, + "Top": 0.876121461391449, + }, + "Polygon": [ + {"X": 0.4310702085494995, "Y": 0.8769540190696716}, + {"X": 0.5673548579216003, "Y": 0.876121461391449}, + {"X": 0.5675443410873413, "Y": 0.90714031457901}, + {"X": 0.4312596917152405, "Y": 0.9079728722572327}, + ], + }, + }, + }, + { + "Timestamp": 0, + "TextDetection": { + "DetectedText": "Hello", + "Type": "WORD", + "Id": 1, + "ParentId": 0, + "Confidence": 99.1568832397461, + "Geometry": { + "BoundingBox": { + "Width": 0.0648193359375, + "Height": 0.0234375, + "Left": 0.43121337890625, + "Top": 0.876953125, + }, + "Polygon": [ + {"X": 0.43121337890625, "Y": 0.876953125}, + {"X": 0.49603271484375, "Y": 0.876953125}, + {"X": 0.49603271484375, "Y": 0.900390625}, + {"X": 0.43121337890625, "Y": 0.900390625}, + ], + }, + }, + }, + { + "Timestamp": 0, + "TextDetection": { + "DetectedText": "world", + "Type": "WORD", + "Id": 2, + "ParentId": 0, + "Confidence": 96.63108825683594, + "Geometry": { + "BoundingBox": { + "Width": 0.07103776931762695, + "Height": 0.02804870530962944, + "Left": 0.4965003430843353, + "Top": 0.8795245885848999, + }, + "Polygon": [ + {"X": 0.4965003430843353, "Y": 0.8809727430343628}, + {"X": 0.5673661231994629, "Y": 0.8795245885848999}, + {"X": 0.5675381422042847, "Y": 0.9061251282691956}, + {"X": 0.4966723322868347, "Y": 0.9075732827186584}, + ], + }, + }, + }, + { + "Timestamp": 1000, + "TextDetection": { + "DetectedText": "Goodbye world", + "Type": "LINE", + "Id": 0, + "Confidence": 98.9729995727539, + "Geometry": { + "BoundingBox": { + "Width": 0.13677978515625, + "Height": 0.0302734375, + "Left": 0.43121337890625, + "Top": 0.876953125, + }, + "Polygon": [ + {"X": 0.43121337890625, "Y": 0.876953125}, + {"X": 0.5679931640625, "Y": 0.876953125}, + {"X": 0.5679931640625, "Y": 0.9072265625}, + {"X": 0.43121337890625, "Y": 0.9072265625}, + ], + }, + }, + }, + { + "Timestamp": 1000, + "TextDetection": { + "DetectedText": "Goodbye", + "Type": "WORD", + "Id": 1, + "ParentId": 0, + "Confidence": 99.7258529663086, + "Geometry": { + "BoundingBox": { + "Width": 0.0648193359375, + "Height": 0.0234375, + "Left": 0.43121337890625, + "Top": 0.876953125, + }, + "Polygon": [ + {"X": 0.43121337890625, "Y": 0.876953125}, + {"X": 0.49603271484375, "Y": 0.876953125}, + {"X": 0.49603271484375, "Y": 0.900390625}, + {"X": 0.43121337890625, "Y": 0.900390625}, + ], + }, + }, + }, + { + "Timestamp": 1000, + "TextDetection": { + "DetectedText": "world", + "Type": "WORD", + "Id": 2, + "ParentId": 0, + "Confidence": 98.22015380859375, + "Geometry": { + "BoundingBox": { + "Width": 0.0703125, + "Height": 0.0263671875, + "Left": 0.4976806640625, + "Top": 0.880859375, + }, + "Polygon": [ + {"X": 0.4976806640625, "Y": 0.880859375}, + {"X": 0.5679931640625, "Y": 0.880859375}, + {"X": 0.5679931640625, "Y": 0.9072265625}, + {"X": 0.4976806640625, "Y": 0.9072265625}, + ], + }, + }, + }, + ] + + +rekognition_backends = BackendDict(RekognitionBackend, "rekognition") diff --git a/moto/rekognition/responses.py b/moto/rekognition/responses.py new file mode 100644 index 000000000..e3d3b2a40 --- /dev/null +++ b/moto/rekognition/responses.py @@ -0,0 +1,45 @@ +"""Handles incoming rekognition requests, invokes methods, returns responses.""" +import json + +from moto.core.responses import BaseResponse +from .models import rekognition_backends + + +class RekognitionResponse(BaseResponse): + """Handler for Rekognition requests and responses.""" + + @property + def rekognition_backend(self): + """Return backend instance specific for this region.""" + return rekognition_backends[self.region] + + def get_text_detection(self): + ( + job_status, + status_message, + video_metadata, + text_detections, + next_token, + text_model_version, + ) = self.rekognition_backend.get_text_detection() + + return json.dumps( + dict( + JobStatus=job_status, + StatusMessage=status_message, + VideoMetadata=video_metadata, + TextDetections=text_detections, + NextToken=next_token, + TextModelVersion=text_model_version, + ) + ) + + def start_text_detection(self): + headers = {"Content-Type": "application/x-amz-json-1.1"} + job_id = self.rekognition_backend.start_text_detection() + response = ('{"JobId":"' + job_id + '"}').encode() + + return 200, headers, response + + +# add templates from here diff --git a/moto/rekognition/urls.py b/moto/rekognition/urls.py new file mode 100644 index 000000000..ce2369472 --- /dev/null +++ b/moto/rekognition/urls.py @@ -0,0 +1,10 @@ +"""rekognition base URL and path.""" +from .responses import RekognitionResponse + +url_bases = [ + r"https?://rekognition\.(.+)\.amazonaws\.com", +] + +url_paths = { + "{0}/$": RekognitionResponse.dispatch, +} diff --git a/tests/test_rekognition/__init__.py b/tests/test_rekognition/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/test_rekognition/test_rekognition.py b/tests/test_rekognition/test_rekognition.py new file mode 100644 index 000000000..9ae3a9581 --- /dev/null +++ b/tests/test_rekognition/test_rekognition.py @@ -0,0 +1,45 @@ +"""Unit tests for rekognition-supported APIs.""" +import random +import string + +import boto3 + +import sure # noqa # pylint: disable=unused-import +from moto import mock_rekognition + +# 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_rekognition +def test_start_text_detection(): + client = boto3.client("rekognition", region_name="ap-southeast-1") + video = { + "S3Object": { + "Bucket": "bucket", + "Name": "key", + } + } + + resp = client.start_text_detection(Video=video) + + resp["ResponseMetadata"]["HTTPStatusCode"].should.equal(200) + + resp.should.have.key("JobId") + + +@mock_rekognition +def test_get_text_detection(): + client = boto3.client("rekognition", region_name="us-east-2") + job_id = "".join( + random.choice(string.ascii_uppercase + string.digits) for _ in range(64) + ) + + resp = client.get_text_detection(JobId=job_id) + + resp["ResponseMetadata"]["HTTPStatusCode"].should.equal(200) + + resp["JobStatus"].should.equal("SUCCEEDED") + resp["TextDetections"][0]["TextDetection"]["DetectedText"].should.equal( + "Hello world" + )