Repository: getmoto/moto
Pull request #7906 (2024-07-30 15:23:34)

Feature: Sagemaker Model Explainability Job Definition
Adds support  for Sagemaker Model Explainability Job Definition create, list, describe, and delete

codecov[bot]: ## [Codecov](https://app.codecov.io/gh/getmoto/moto/pull/7906?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto) Report
Attention: Patch coverage is `96.55172%` with `3 lines` in your changes missing coverage. Please review.
> Project coverage is 94.38%. Comparing base [(`9fbf373`)](https://app.codecov.io/gh/getmoto/moto/commit/9fbf37381619ce8fa16d42755b214ef79482bcb4?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto) to head [(`350cc6e`)](https://app.codecov.io/gh/getmoto/moto/commit/350cc6e397747c8693c734cf867f35325df3c870?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto).

| [Files](https://app.codecov.io/gh/getmoto/moto/pull/7906?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto) | Patch % | Lines |
|---|---|---|
| [moto/sagemaker/models.py](https://app.codecov.io/gh/getmoto/moto/pull/7906?src=pr&el=tree&filepath=moto%2Fsagemaker%2Fmodels.py&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto#diff-bW90by9zYWdlbWFrZXIvbW9kZWxzLnB5) | 94.44% | [3 Missing :warning: ](https://app.codecov.io/gh/getmoto/moto/pull/7906?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto) |

<details><summary>Additional details and impacted files</summary>


```diff
@@           Coverage Diff           @@
##           master    #7906   +/-   ##
=======================================
  Coverage   94.37%   94.38%           
=======================================
  Files        1114     1114           
  Lines       95137    95224   +87     
=======================================
+ Hits        89790    89875   +85     
- Misses       5347     5349    +2     
```

| [Flag](https://app.codecov.io/gh/getmoto/moto/pull/7906/flags?src=pr&el=flags&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto) | Coverage Δ | |
|---|---|---|
| [servertests](https://app.codecov.io/gh/getmoto/moto/pull/7906/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto) | `29.15% <11.49%> (-0.02%)` | :arrow_down: |
| [unittests](https://app.codecov.io/gh/getmoto/moto/pull/7906/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto) | `94.35% <96.55%> (+<0.01%)` | :arrow_up: |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto#carryforward-flags-in-the-pull-request-comment) to find out more.

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/getmoto/moto/pull/7906?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto).


Patch:
diff --git a/moto/rds/models.py b/moto/rds/models.py
index 3e14fa973..d22b188c9 100644
--- a/moto/rds/models.py
+++ b/moto/rds/models.py
@@ -1538,7 +1538,7 @@ class DBProxy(RDSBaseModel):
         super().__init__(backend)
         self.db_proxy_name = db_proxy_name
         self.engine_family = engine_family
-        if self.engine_family not in ["MYSQL", "POSTGRESQ", "SQLSERVER"]:
+        if self.engine_family not in ["MYSQL", "POSTGRESQL", "SQLSERVER"]:
             raise InvalidParameterValue("Provided EngineFamily is not valid.")
         self.auth = auth
         self.role_arn = role_arn
diff --git a/moto/sagemaker/models.py b/moto/sagemaker/models.py
index ec9816958..c97c1a6d8 100644
--- a/moto/sagemaker/models.py
+++ b/moto/sagemaker/models.py
@@ -115,6 +115,12 @@ PAGINATION_MODEL = {
         "limit_default": 100,
         "unique_attribute": "arn",
     },
+    "list_model_explainability_job_definitions": {
+        "input_token": "next_token",
+        "limit_key": "max_results",
+        "limit_default": 100,
+        "unique_attribute": "arn",
+    },
 }
 
 METRIC_INFO_TYPE = Dict[str, Union[str, int, float, datetime]]
@@ -2093,6 +2099,83 @@ class Domain(BaseObject):
         }
 
 
+class ModelExplainabilityJobDefinition(BaseObject):
+    def __init__(
+        self,
+        job_definition_name: str,
+        model_explainability_baseline_config: Optional[Dict[str, Any]],
+        model_explainability_app_specification: Dict[str, Any],
+        model_explainability_job_input: Dict[str, Any],
+        model_explainability_job_output_config: Dict[str, Any],
+        job_resources: Dict[str, Any],
+        network_config: Optional[Dict[str, Any]],
+        role_arn: str,
+        stopping_condition: Optional[Dict[str, Any]],
+        region_name: str,
+        account_id: str,
+        tags: Optional[List[Dict[str, str]]],
+    ):
+        self.job_definition_name = job_definition_name
+        if (
+            job_definition_name
+            in sagemaker_backends[account_id][
+                region_name
+            ].model_explainability_job_definitions
+        ):
+            raise ResourceInUseException(
+                message=f"Resource Already Exists: ModelExplainabilityJobDefinition with name {job_definition_name} already exists. Choose a different name."
+            )
+        self.model_explainability_baseline_config = model_explainability_baseline_config
+        self.model_explainability_app_specification = (
+            model_explainability_app_specification
+        )
+        self.model_explainability_job_input = model_explainability_job_input
+        self.model_explainability_job_output_config = (
+            model_explainability_job_output_config
+        )
+        self.job_resources = job_resources
+        self.network_config = network_config
+        self.role_arn = role_arn
+        self.stopping_condition = stopping_condition
+        self.region_name = region_name
+        self.account_id = account_id
+        self.tags = tags
+
+        self.arn = arn_formatter(
+            "model-explainability-job-definition",
+            job_definition_name,
+            self.account_id,
+            self.region_name,
+        )
+        self.creation_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
+        self.endpoint_name = model_explainability_job_input["EndpointInput"][
+            "EndpointName"
+        ]
+
+    def describe(self) -> Dict[str, Any]:
+        return {
+            "JobDefinitionArn": self.arn,
+            "JobDefinitionName": self.job_definition_name,
+            "CreationTime": self.creation_time,
+            "ModelExplainabilityBaselineConfig": self.model_explainability_baseline_config,
+            "ModelExplainabilityAppSpecification": self.model_explainability_app_specification,
+            "ModelExplainabilityJobInput": self.model_explainability_job_input,
+            "ModelExplainabilityJobOutputConfig": self.model_explainability_job_output_config,
+            "JobResources": self.job_resources,
+            "NetworkConfig": self.network_config,
+            "RoleArn": self.role_arn,
+            "StoppingConditions": self.stopping_condition,
+        }
+
+    def summary(self) -> Dict[str, Any]:
+        return {
+            "MonitoringJobDefinitionName": self.job_definition_name,
+            "MonitoringJobDefinitionArn": self.arn,
+            "CreationTime": self.creation_time,
+            "EndpointName": self.endpoint_name,
+        }
+
+
 class VpcConfig(BaseObject):
     def __init__(self, security_group_ids: List[str], subnets: List[str]):
         self.security_group_ids = security_group_ids
@@ -2490,6 +2573,9 @@ class SageMakerModelBackend(BaseBackend):
         self.auto_ml_jobs: Dict[str, AutoMLJob] = {}
         self.compilation_jobs: Dict[str, CompilationJob] = {}
         self.domains: Dict[str, Domain] = {}
+        self.model_explainability_job_definitions: Dict[
+            str, ModelExplainabilityJobDefinition
+        ] = {}
 
     @staticmethod
     def default_vpc_endpoint_service(
@@ -2623,6 +2709,7 @@ class SageMakerModelBackend(BaseBackend):
             "automl-job": self.auto_ml_jobs,
             "compilation-job": self.compilation_jobs,
             "domain": self.domains,
+            "model-explainability-job-definition": self.model_explainability_job_definitions,
         }
         target_resource, target_name = arn.split(":")[-1].split("/")
         try:
@@ -4809,6 +4896,109 @@ class SageMakerModelBackend(BaseBackend):
             raise ValidationError(message=f"Could not find domain '{domain_id}'.")
         del self.domains[domain_id]
 
+    def create_model_explainability_job_definition(
+        self,
+        job_definition_name: str,
+        model_explainability_baseline_config: Optional[Dict[str, Any]],
+        model_explainability_app_specification: Dict[str, Any],
+        model_explainability_job_input: Dict[str, Any],
+        model_explainability_job_output_config: Dict[str, Any],
+        job_resources: Dict[str, Any],
+        network_config: Optional[Dict[str, Any]],
+        role_arn: str,
+        stopping_condition: Optional[Dict[str, Any]],
+        tags: List[Dict[str, str]],
+    ) -> str:
+        model_explainability_job_definition = ModelExplainabilityJobDefinition(
+            job_definition_name=job_definition_name,
+            model_explainability_baseline_config=model_explainability_baseline_config,
+            model_explainability_app_specification=model_explainability_app_specification,
+            model_explainability_job_input=model_explainability_job_input,
+            model_explainability_job_output_config=model_explainability_job_output_config,
+            job_resources=job_resources,
+            region_name=self.region_name,
+            account_id=self.account_id,
+            network_config=network_config,
+            role_arn=role_arn,
+            stopping_condition=stopping_condition,
+            tags=tags,
+        )
+        self.model_explainability_job_definitions[
+            model_explainability_job_definition.job_definition_name
+        ] = model_explainability_job_definition
+        return model_explainability_job_definition.arn
+
+    def describe_model_explainability_job_definition(
+        self, job_definition_name: str
+    ) -> Dict[str, Any]:
+        if job_definition_name not in self.model_explainability_job_definitions:
+            raise ResourceNotFound(
+                message=f"Could not find model explainability job definition with name '{job_definition_name}'."
+            )
+        return self.model_explainability_job_definitions[job_definition_name].describe()
+
+    @paginate(pagination_model=PAGINATION_MODEL)
+    def list_model_explainability_job_definitions(
+        self,
+        endpoint_name: Optional[str],
+        sort_by: Optional[str],
+        sort_order: Optional[str],
+        name_contains: Optional[str],
+        creation_time_before: Optional[str],
+        creation_time_after: Optional[str],
+    ) -> List[ModelExplainabilityJobDefinition]:
+        model_explainability_job_definitions = list(
+            self.model_explainability_job_definitions.values()
+        )
+        if endpoint_name:
+            model_explainability_job_definitions = [
+                i
+                for i in model_explainability_job_definitions
+                if endpoint_name == i.endpoint_name
+            ]
+        if name_contains:
+            model_explainability_job_definitions = [
+                i
+                for i in model_explainability_job_definitions
+                if name_contains in i.job_definition_name
+            ]
+        if creation_time_before:
+            model_explainability_job_definitions = [
+                i
+                for i in model_explainability_job_definitions
+                if i.creation_time < str(creation_time_before)
+            ]
+        if creation_time_after:
+            model_explainability_job_definitions = [
+                i
+                for i in model_explainability_job_definitions
+                if i.creation_time > str(creation_time_after)
+            ]
+        reverse = sort_order == "Descending"
+        if sort_by == "Name":
+            model_explainability_job_definitions = sorted(
+                model_explainability_job_definitions,
+                key=lambda x: x.job_definition_name,
+                reverse=reverse,
+            )
+        if sort_by == "CreationTime" or sort_by is None:
+            model_explainability_job_definitions = sorted(
+                model_explainability_job_definitions,
+                key=lambda x: x.creation_time,
+                reverse=reverse,
+            )
+        return model_explainability_job_definitions
+
+    def delete_model_explainability_job_definition(
+        self, job_definition_name: str
+    ) -> None:
+        if job_definition_name not in self.model_explainability_job_definitions:
+            raise ResourceNotFound(
+                message=f"Could not find model explainability job definition with name '{job_definition_name}'."
+            )
+        del self.model_explainability_job_definitions[job_definition_name]
+        return
+
 
 class FakeExperiment(BaseObject):
     def __init__(
diff --git a/moto/sagemaker/responses.py b/moto/sagemaker/responses.py
index 49fc687c9..1e8ee626f 100644
--- a/moto/sagemaker/responses.py
+++ b/moto/sagemaker/responses.py
@@ -1260,3 +1260,76 @@ class SageMakerResponse(BaseResponse):
             retention_policy=retention_policy,
         )
         return json.dumps(dict())
+
+    def create_model_explainability_job_definition(self) -> str:
+        job_definition_name = self._get_param("JobDefinitionName")
+        model_explainability_baseline_config = self._get_param(
+            "ModelExplainabilityBaselineConfig"
+        )
+        model_explainability_app_specification = self._get_param(
+            "ModelExplainabilityAppSpecification"
+        )
+        model_explainability_job_input = self._get_param("ModelExplainabilityJobInput")
+        model_explainability_job_output_config = self._get_param(
+            "ModelExplainabilityJobOutputConfig"
+        )
+        job_resources = self._get_param("JobResources")
+        network_config = self._get_param("NetworkConfig")
+        role_arn = self._get_param("RoleArn")
+        stopping_condition = self._get_param("StoppingCondition")
+        tags = self._get_param("Tags")
+        job_definition_arn = self.sagemaker_backend.create_model_explainability_job_definition(
+            job_definition_name=job_definition_name,
+            model_explainability_baseline_config=model_explainability_baseline_config,
+            model_explainability_app_specification=model_explainability_app_specification,
+            model_explainability_job_input=model_explainability_job_input,
+            model_explainability_job_output_config=model_explainability_job_output_config,
+            job_resources=job_resources,
+            network_config=network_config,
+            role_arn=role_arn,
+            stopping_condition=stopping_condition,
+            tags=tags,
+        )
+        return json.dumps(dict(JobDefinitionArn=job_definition_arn))
+
+    def describe_model_explainability_job_definition(self) -> str:
+        job_definition_name = self._get_param("JobDefinitionName")
+        description = (
+            self.sagemaker_backend.describe_model_explainability_job_definition(
+                job_definition_name=job_definition_name,
+            )
+        )
+        return json.dumps(description)
+
+    def list_model_explainability_job_definitions(self) -> str:
+        endpoint_name = self._get_param("EndpointName")
+        sort_by = self._get_param("SortBy")
+        sort_order = self._get_param("SortOrder")
+        next_token = self._get_param("NextToken")
+        max_results = self._get_param("MaxResults")
+        name_contains = self._get_param("NameContains")
+        creation_time_before = self._get_param("CreationTimeBefore")
+        creation_time_after = self._get_param("CreationTimeAfter")
+        job_definitions, next_token = (
+            self.sagemaker_backend.list_model_explainability_job_definitions(
+                endpoint_name=endpoint_name,
+                sort_by=sort_by,
+                sort_order=sort_order,
+                next_token=next_token,
+                max_results=max_results,
+                name_contains=name_contains,
+                creation_time_before=creation_time_before,
+                creation_time_after=creation_time_after,
+            )
+        )
+        job_definition_summaries = [job.summary() for job in job_definitions]
+        return json.dumps(
+            dict(JobDefinitionSummaries=job_definition_summaries, NextToken=next_token)
+        )
+
+    def delete_model_explainability_job_definition(self) -> str:
+        job_definition_name = self._get_param("JobDefinitionName")
+        self.sagemaker_backend.delete_model_explainability_job_definition(
+            job_definition_name=job_definition_name,
+        )
+        return json.dumps(dict())


Test patch:
diff --git a/tests/test_sagemaker/test_sagemaker_model_expainability.py b/tests/test_sagemaker/test_sagemaker_model_expainability.py
new file mode 100644
index 000000000..8302890a4
--- /dev/null
+++ b/tests/test_sagemaker/test_sagemaker_model_expainability.py
@@ -0,0 +1,565 @@
+"""Unit tests for sagemaker-supported APIs."""
+
+import datetime
+
+import boto3
+
+from moto import mock_aws
+from moto.core import DEFAULT_ACCOUNT_ID as 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_aws
+def test_create_model_explainability_job_definition():
+    client = boto3.client("sagemaker", region_name="eu-west-1")
+    arn = client.create_model_explainability_job_definition(
+        JobDefinitionName="testmodelexplainabilityjobdefinition",
+        ModelExplainabilityBaselineConfig={
+            "BaseliningJobName": "testbaseliningjobname",
+            "ConstraintsResource": {"S3Uri": "s3://constrain/resource"},
+        },
+        ModelExplainabilityAppSpecification={
+            "ImageUri": "testimageuri",
+            "ConfigUri": "testconfiguri",
+            "Environment": {"test": "test"},
+        },
+        ModelExplainabilityJobInput={
+            "EndpointInput": {
+                "EndpointName": "testendpointname",
+                "LocalPath": "testlocalpath",
+                "S3DataDistributionType": "FullyReplicated",
+                "S3InputMode": "File",
+            },
+        },
+        ModelExplainabilityJobOutputConfig={
+            "MonitoringOutputs": [
+                {
+                    "S3Output": {
+                        "S3Uri": "s3://output/uri",
+                        "LocalPath": "testlocalpath",
+                        "S3UploadMode": "Continuous",
+                    }
+                },
+            ]
+        },
+        JobResources={
+            "ClusterConfig": {
+                "InstanceCount": 1,
+                "InstanceType": "ml.m4.xlarge",
+                "VolumeSizeInGB": 4,
+            },
+        },
+        NetworkConfig={"EnableInterContainerTrafficEncryption": True},
+        RoleArn=f"arn:aws:iam::{ACCOUNT_ID}:role/FakeRole",
+        StoppingCondition={"MaxRuntimeInSeconds": 123},
+        Tags=[{"Key": "testkey", "Value": "testvalue"}],
+    )
+    assert (
+        arn["JobDefinitionArn"]
+        == f"arn:aws:sagemaker:eu-west-1:{ACCOUNT_ID}:model-explainability-job-definition/testmodelexplainabilityjobdefinition"
+    )
+
+
+@mock_aws
+def test_describe_model_explainability_job_definition():
+    client = boto3.client("sagemaker", region_name="eu-west-1")
+    client.create_model_explainability_job_definition(
+        JobDefinitionName="testmodelexplainabilityjobdefinition",
+        ModelExplainabilityBaselineConfig={
+            "BaseliningJobName": "testbaseliningjobname",
+            "ConstraintsResource": {"S3Uri": "s3://constrain/resource"},
+        },
+        ModelExplainabilityAppSpecification={
+            "ImageUri": "testimageuri",
+            "ConfigUri": "testconfiguri",
+            "Environment": {"test": "test"},
+        },
+        ModelExplainabilityJobInput={
+            "EndpointInput": {
+                "EndpointName": "testendpointname",
+                "LocalPath": "testlocalpath",
+                "S3DataDistributionType": "FullyReplicated",
+                "S3InputMode": "File",
+            },
+        },
+        ModelExplainabilityJobOutputConfig={
+            "MonitoringOutputs": [
+                {
+                    "S3Output": {
+                        "S3Uri": "s3://output/uri",
+                        "LocalPath": "testlocalpath",
+                        "S3UploadMode": "Continuous",
+                    }
+                },
+            ]
+        },
+        JobResources={
+            "ClusterConfig": {
+                "InstanceCount": 1,
+                "InstanceType": "ml.m4.xlarge",
+                "VolumeSizeInGB": 4,
+            },
+        },
+        NetworkConfig={"EnableInterContainerTrafficEncryption": True},
+        RoleArn=f"arn:aws:iam::{ACCOUNT_ID}:role/FakeRole",
+        StoppingCondition={"MaxRuntimeInSeconds": 123},
+        Tags=[{"Key": "testkey", "Value": "testvalue"}],
+    )
+    resp = client.describe_model_explainability_job_definition(
+        JobDefinitionName="testmodelexplainabilityjobdefinition"
+    )
+    assert (
+        resp["JobDefinitionArn"]
+        == f"arn:aws:sagemaker:eu-west-1:{ACCOUNT_ID}:model-explainability-job-definition/testmodelexplainabilityjobdefinition"
+    )
+    assert resp["JobDefinitionName"] == "testmodelexplainabilityjobdefinition"
+    assert resp["ModelExplainabilityBaselineConfig"] == {
+        "BaseliningJobName": "testbaseliningjobname",
+        "ConstraintsResource": {"S3Uri": "s3://constrain/resource"},
+    }
+    assert resp["ModelExplainabilityAppSpecification"] == {
+        "ImageUri": "testimageuri",
+        "ConfigUri": "testconfiguri",
+        "Environment": {"test": "test"},
+    }
+    assert resp["ModelExplainabilityJobInput"] == {
+        "EndpointInput": {
+            "EndpointName": "testendpointname",
+            "LocalPath": "testlocalpath",
+            "S3DataDistributionType": "FullyReplicated",
+            "S3InputMode": "File",
+        },
+    }
+    assert resp["ModelExplainabilityJobOutputConfig"] == {
+        "MonitoringOutputs": [
+            {
+                "S3Output": {
+                    "S3Uri": "s3://output/uri",
+                    "LocalPath": "testlocalpath",
+                    "S3UploadMode": "Continuous",
+                }
+            },
+        ]
+    }
+    assert resp["JobResources"] == {
+        "ClusterConfig": {
+            "InstanceCount": 1,
+            "InstanceType": "ml.m4.xlarge",
+            "VolumeSizeInGB": 4,
+        },
+    }
+    assert resp["NetworkConfig"] == {"EnableInterContainerTrafficEncryption": True}
+    assert resp["RoleArn"] == f"arn:aws:iam::{ACCOUNT_ID}:role/FakeRole"
+
+
+@mock_aws
+def test_describe_model_explainability_job_definition_defaults():
+    client = boto3.client("sagemaker", region_name="eu-west-1")
+    client.create_model_explainability_job_definition(
+        JobDefinitionName="testmodelexplainabilityjobdefinition",
+        ModelExplainabilityAppSpecification={
+            "ImageUri": "testimageuri",
+            "ConfigUri": "testconfiguri",
+        },
+        ModelExplainabilityJobInput={
+            "EndpointInput": {
+                "EndpointName": "testendpointname",
+                "LocalPath": "testlocalpath",
+            },
+        },
+        ModelExplainabilityJobOutputConfig={
+            "MonitoringOutputs": [
+                {
+                    "S3Output": {
+                        "S3Uri": "s3://output/uri",
+                        "LocalPath": "testlocalpath",
+                    }
+                },
+            ]
+        },
+        JobResources={
+            "ClusterConfig": {
+                "InstanceCount": 1,
+                "InstanceType": "ml.m4.xlarge",
+                "VolumeSizeInGB": 4,
+            },
+        },
+        RoleArn=f"arn:aws:iam::{ACCOUNT_ID}:role/FakeRole",
+    )
+    resp = client.describe_model_explainability_job_definition(
+        JobDefinitionName="testmodelexplainabilityjobdefinition"
+    )
+    assert (
+        resp["JobDefinitionArn"]
+        == f"arn:aws:sagemaker:eu-west-1:{ACCOUNT_ID}:model-explainability-job-definition/testmodelexplainabilityjobdefinition"
+    )
+    assert resp["JobDefinitionName"] == "testmodelexplainabilityjobdefinition"
+    assert resp["ModelExplainabilityAppSpecification"] == {
+        "ImageUri": "testimageuri",
+        "ConfigUri": "testconfiguri",
+    }
+    assert resp["ModelExplainabilityJobInput"] == {
+        "EndpointInput": {
+            "EndpointName": "testendpointname",
+            "LocalPath": "testlocalpath",
+        },
+    }
+    assert resp["ModelExplainabilityJobOutputConfig"] == {
+        "MonitoringOutputs": [
+            {
+                "S3Output": {
+                    "S3Uri": "s3://output/uri",
+                    "LocalPath": "testlocalpath",
+                }
+            },
+        ]
+    }
+    assert resp["JobResources"] == {
+        "ClusterConfig": {
+            "InstanceCount": 1,
+            "InstanceType": "ml.m4.xlarge",
+            "VolumeSizeInGB": 4,
+        },
+    }
+    assert resp["RoleArn"] == f"arn:aws:iam::{ACCOUNT_ID}:role/FakeRole"
+
+
+@mock_aws
+def test_list_model_explainability_job_definitions():
+    client = boto3.client("sagemaker", region_name="eu-west-1")
+    client.create_model_explainability_job_definition(
+        JobDefinitionName="testmodelexplainabilityjobdefinition",
+        ModelExplainabilityAppSpecification={
+            "ImageUri": "testimageuri",
+            "ConfigUri": "testconfiguri",
+        },
+        ModelExplainabilityJobInput={
+            "EndpointInput": {
+                "EndpointName": "testendpointname",
+                "LocalPath": "testlocalpath",
+            },
+        },
+        ModelExplainabilityJobOutputConfig={
+            "MonitoringOutputs": [
+                {
+                    "S3Output": {
+                        "S3Uri": "s3://output/uri",
+                        "LocalPath": "testlocalpath",
+                    }
+                },
+            ]
+        },
+        JobResources={
+            "ClusterConfig": {
+                "InstanceCount": 1,
+                "InstanceType": "ml.m4.xlarge",
+                "VolumeSizeInGB": 4,
+            },
+        },
+        RoleArn=f"arn:aws:iam::{ACCOUNT_ID}:role/FakeRole",
+    )
+    client.create_model_explainability_job_definition(
+        JobDefinitionName="testmodelexplainabilityjobdefinition2",
+        ModelExplainabilityAppSpecification={
+            "ImageUri": "testimageuri",
+            "ConfigUri": "testconfiguri",
+        },
+        ModelExplainabilityJobInput={
+            "EndpointInput": {
+                "EndpointName": "testendpointname2",
+                "LocalPath": "testlocalpath",
+            },
+        },
+        ModelExplainabilityJobOutputConfig={
+            "MonitoringOutputs": [
+                {
+                    "S3Output": {
+                        "S3Uri": "s3://output/uri",
+                        "LocalPath": "testlocalpath",
+                    }
+                },
+            ]
+        },
+        JobResources={
+            "ClusterConfig": {
+                "InstanceCount": 1,
+                "InstanceType": "ml.m4.xlarge",
+                "VolumeSizeInGB": 4,
+            },
+        },
+        RoleArn=f"arn:aws:iam::{ACCOUNT_ID}:role/FakeRole",
+    )
+    resp = client.list_model_explainability_job_definitions()["JobDefinitionSummaries"]
+    assert (
+        resp[0]["MonitoringJobDefinitionName"] == "testmodelexplainabilityjobdefinition"
+    )
+    assert (
+        resp[0]["MonitoringJobDefinitionArn"]
+        == f"arn:aws:sagemaker:eu-west-1:{ACCOUNT_ID}:model-explainability-job-definition/testmodelexplainabilityjobdefinition"
+    )
+    assert isinstance(resp[0]["CreationTime"], datetime.datetime)
+    assert resp[0]["EndpointName"] == "testendpointname"
+    assert (
+        resp[1]["MonitoringJobDefinitionName"]
+        == "testmodelexplainabilityjobdefinition2"
+    )
+    assert (
+        resp[1]["MonitoringJobDefinitionArn"]
+        == f"arn:aws:sagemaker:eu-west-1:{ACCOUNT_ID}:model-explainability-job-definition/testmodelexplainabilityjobdefinition2"
+    )
+    assert isinstance(resp[1]["CreationTime"], datetime.datetime)
+    assert resp[1]["EndpointName"] == "testendpointname2"
+
+
+@mock_aws
+def test_list_model_explainability_job_definitions_filters():
+    client = boto3.client("sagemaker", region_name="eu-west-1")
+    client.create_model_explainability_job_definition(
+        JobDefinitionName="testmodelexplainabilityjobdefinition",
+        ModelExplainabilityAppSpecification={
+            "ImageUri": "testimageuri",
+            "ConfigUri": "testconfiguri",
+        },
+        ModelExplainabilityJobInput={
+            "EndpointInput": {
+                "EndpointName": "testendpointname",
+                "LocalPath": "testlocalpath",
+            },
+        },
+        ModelExplainabilityJobOutputConfig={
+            "MonitoringOutputs": [
+                {
+                    "S3Output": {
+                        "S3Uri": "s3://output/uri",
+                        "LocalPath": "testlocalpath",
+                    }
+                },
+            ]
+        },
+        JobResources={
+            "ClusterConfig": {
+                "InstanceCount": 1,
+                "InstanceType": "ml.m4.xlarge",
+                "VolumeSizeInGB": 4,
+            },
+        },
+        RoleArn=f"arn:aws:iam::{ACCOUNT_ID}:role/FakeRole",
+    )
+    client.create_model_explainability_job_definition(
+        JobDefinitionName="test2modelexplainabilityjobdefinition2",
+        ModelExplainabilityAppSpecification={
+            "ImageUri": "testimageuri",
+            "ConfigUri": "testconfiguri",
+        },
+        ModelExplainabilityJobInput={
+            "EndpointInput": {
+                "EndpointName": "testendpointname",
+                "LocalPath": "testlocalpath",
+            },
+        },
+        ModelExplainabilityJobOutputConfig={
+            "MonitoringOutputs": [
+                {
+                    "S3Output": {
+                        "S3Uri": "s3://output/uri",
+                        "LocalPath": "testlocalpath",
+                    }
+                },
+            ]
+        },
+        JobResources={
+            "ClusterConfig": {
+                "InstanceCount": 1,
+                "InstanceType": "ml.m4.xlarge",
+                "VolumeSizeInGB": 4,
+            },
+        },
+        RoleArn=f"arn:aws:iam::{ACCOUNT_ID}:role/FakeRole",
+    )
+    client.create_model_explainability_job_definition(
+        JobDefinitionName="testmodelexplainabilityjobdefinition3",
+        ModelExplainabilityAppSpecification={
+            "ImageUri": "testimageuri",
+            "ConfigUri": "testconfiguri",
+        },
+        ModelExplainabilityJobInput={
+            "EndpointInput": {
+                "EndpointName": "test3endpointname3",
+                "LocalPath": "testlocalpath",
+            },
+        },
+        ModelExplainabilityJobOutputConfig={
+            "MonitoringOutputs": [
+                {
+                    "S3Output": {
+                        "S3Uri": "s3://output/uri",
+                        "LocalPath": "testlocalpath",
+                    }
+                },
+            ]
+        },
+        JobResources={
+            "ClusterConfig": {
+                "InstanceCount": 1,
+                "InstanceType": "ml.m4.xlarge",
+                "VolumeSizeInGB": 4,
+            },
+        },
+        RoleArn=f"arn:aws:iam::{ACCOUNT_ID}:role/FakeRole",
+    )
+    resp = client.list_model_explainability_job_definitions(
+        EndpointName="testendpointname",
+        SortBy="Name",
+        SortOrder="Ascending",
+        NameContains="testmodelexplainabilityjobdefinition",
+        CreationTimeBefore=str(datetime.datetime(2099, 1, 1)),
+        CreationTimeAfter=str(datetime.datetime(2021, 1, 1)),
+    )["JobDefinitionSummaries"]
+    assert len(resp) == 1
+    assert (
+        resp[0]["MonitoringJobDefinitionName"] == "testmodelexplainabilityjobdefinition"
+    )
+    assert (
+        resp[0]["MonitoringJobDefinitionArn"]
+        == f"arn:aws:sagemaker:eu-west-1:{ACCOUNT_ID}:model-explainability-job-definition/testmodelexplainabilityjobdefinition"
+    )
+    assert isinstance(resp[0]["CreationTime"], datetime.datetime)
+    assert resp[0]["EndpointName"] == "testendpointname"
+
+
+@mock_aws
+def test_delete_model_explainability_job_definition():
+    client = boto3.client("sagemaker", region_name="eu-west-1")
+    client.create_model_explainability_job_definition(
+        JobDefinitionName="testmodelexplainabilityjobdefinition",
+        ModelExplainabilityAppSpecification={
+            "ImageUri": "testimageuri",
+            "ConfigUri": "testconfiguri",
+        },
+        ModelExplainabilityJobInput={
+            "EndpointInput": {
+                "EndpointName": "testendpointname",
+                "LocalPath": "testlocalpath",
+            },
+        },
+        ModelExplainabilityJobOutputConfig={
+            "MonitoringOutputs": [
+                {
+                    "S3Output": {
+                        "S3Uri": "s3://output/uri",
+                        "LocalPath": "testlocalpath",
+                    }
+                },
+            ]
+        },
+        JobResources={
+            "ClusterConfig": {
+                "InstanceCount": 1,
+                "InstanceType": "ml.m4.xlarge",
+                "VolumeSizeInGB": 4,
+            },
+        },
+        RoleArn=f"arn:aws:iam::{ACCOUNT_ID}:role/FakeRole",
+    )
+    client.create_model_explainability_job_definition(
+        JobDefinitionName="test2modelexplainabilityjobdefinition2",
+        ModelExplainabilityAppSpecification={
+            "ImageUri": "testimageuri",
+            "ConfigUri": "testconfiguri",
+        },
+        ModelExplainabilityJobInput={
+            "EndpointInput": {
+                "EndpointName": "testendpointname",
+                "LocalPath": "testlocalpath",
+            },
+        },
+        ModelExplainabilityJobOutputConfig={
+            "MonitoringOutputs": [
+                {
+                    "S3Output": {
+                        "S3Uri": "s3://output/uri",
+                        "LocalPath": "testlocalpath",
+                    }
+                },
+            ]
+        },
+        JobResources={
+            "ClusterConfig": {
+                "InstanceCount": 1,
+                "InstanceType": "ml.m4.xlarge",
+                "VolumeSizeInGB": 4,
+            },
+        },
+        RoleArn=f"arn:aws:iam::{ACCOUNT_ID}:role/FakeRole",
+    )
+    client.delete_model_explainability_job_definition(
+        JobDefinitionName="testmodelexplainabilityjobdefinition"
+    )
+    resp = client.list_model_explainability_job_definitions()
+    assert len(resp["JobDefinitionSummaries"]) == 1
+    assert (
+        resp["JobDefinitionSummaries"][0]["MonitoringJobDefinitionName"]
+        == "test2modelexplainabilityjobdefinition2"
+    )
+
+
+@mock_aws
+def test_tag_model_explainability_job_definition():
+    client = boto3.client("sagemaker", region_name="eu-west-1")
+    client.create_model_explainability_job_definition(
+        JobDefinitionName="testmodelexplainabilityjobdefinition",
+        ModelExplainabilityAppSpecification={
+            "ImageUri": "testimageuri",
+            "ConfigUri": "testconfiguri",
+        },
+        ModelExplainabilityJobInput={
+            "EndpointInput": {
+                "EndpointName": "testendpointname",
+                "LocalPath": "testlocalpath",
+            },
+        },
+        ModelExplainabilityJobOutputConfig={
+            "MonitoringOutputs": [
+                {
+                    "S3Output": {
+                        "S3Uri": "s3://output/uri",
+                        "LocalPath": "testlocalpath",
+                    }
+                },
+            ]
+        },
+        JobResources={
+            "ClusterConfig": {
+                "InstanceCount": 1,
+                "InstanceType": "ml.m4.xlarge",
+                "VolumeSizeInGB": 4,
+            },
+        },
+        RoleArn=f"arn:aws:iam::{ACCOUNT_ID}:role/FakeRole",
+        Tags=[{"Key": "testkey", "Value": "testvalue"}],
+    )
+    resp1 = client.list_tags(
+        ResourceArn=f"arn:aws:sagemaker:eu-west-1:{ACCOUNT_ID}:model-explainability-job-definition/testmodelexplainabilityjobdefinition"
+    )
+    assert resp1["Tags"] == [{"Key": "testkey", "Value": "testvalue"}]
+    client.add_tags(
+        ResourceArn=f"arn:aws:sagemaker:eu-west-1:{ACCOUNT_ID}:model-explainability-job-definition/testmodelexplainabilityjobdefinition",
+        Tags=[{"Key": "testkey2", "Value": "testvalue2"}],
+    )
+    resp2 = client.list_tags(
+        ResourceArn=f"arn:aws:sagemaker:eu-west-1:{ACCOUNT_ID}:model-explainability-job-definition/testmodelexplainabilityjobdefinition"
+    )
+    assert resp2["Tags"] == [
+        {"Key": "testkey", "Value": "testvalue"},
+        {"Key": "testkey2", "Value": "testvalue2"},
+    ]
+    client.delete_tags(
+        ResourceArn=f"arn:aws:sagemaker:eu-west-1:{ACCOUNT_ID}:model-explainability-job-definition/testmodelexplainabilityjobdefinition",
+        TagKeys=["testkey"],
+    )
+    resp3 = client.list_tags(
+        ResourceArn=f"arn:aws:sagemaker:eu-west-1:{ACCOUNT_ID}:model-explainability-job-definition/testmodelexplainabilityjobdefinition"
+    )
+    assert resp3["Tags"] == [{"Key": "testkey2", "Value": "testvalue2"}]