import json
import os
import random
import re
import string
from collections import defaultdict
from collections.abc import Iterable
from datetime import datetime, timezone
from typing import Any, cast

from moto.core.base_backend import BackendDict, BaseBackend
from moto.core.common_models import BaseModel, CloudFormationModel
from moto.core.utils import camelcase_to_underscores, utcnow
from moto.sagemaker import validators
from moto.utilities.paginator import paginate
from moto.utilities.utils import ARN_PARTITION_REGEX, get_partition

from .exceptions import (
    AWSValidationException,
    ConflictException,
    MissingModel,
    ResourceInUseException,
    ResourceNotFound,
    ValidationError,
)
from .utils import (
    arn_formatter,
    filter_model_cards,
    get_pipeline_execution_from_arn,
    get_pipeline_from_name,
    get_pipeline_name_from_execution_arn,
    load_pipeline_definition_from_s3,
    validate_model_approval_status,
)

PAGINATION_MODEL = {
    "list_experiments": {
        "input_token": "NextToken",
        "limit_key": "MaxResults",
        "limit_default": 100,
        "unique_attribute": "arn",
    },
    "list_trials": {
        "input_token": "NextToken",
        "limit_key": "MaxResults",
        "limit_default": 100,
        "unique_attribute": "arn",
    },
    "list_trial_components": {
        "input_token": "NextToken",
        "limit_key": "MaxResults",
        "limit_default": 100,
        "unique_attribute": "arn",
    },
    "list_tags": {
        "input_token": "NextToken",
        "limit_key": "MaxResults",
        "limit_default": 50,
        "unique_attribute": "Key",
    },
    "list_model_package_groups": {
        "input_token": "next_token",
        "limit_key": "max_results",
        "limit_default": 100,
        "unique_attribute": "arn",
    },
    "list_model_packages": {
        "input_token": "next_token",
        "limit_key": "max_results",
        "limit_default": 100,
        "unique_attribute": "arn",
    },
    "list_notebook_instances": {
        "input_token": "next_token",
        "limit_key": "max_results",
        "limit_default": 100,
        "unique_attribute": "arn",
    },
    "list_clusters": {
        "input_token": "next_token",
        "limit_key": "max_results",
        "limit_default": 100,
        "unique_attribute": "arn",
    },
    "list_cluster_nodes": {
        "input_token": "next_token",
        "limit_key": "max_results",
        "limit_default": 100,
        "unique_attribute": "arn",
    },
    "list_auto_ml_jobs": {
        "input_token": "next_token",
        "limit_key": "max_results",
        "limit_default": 100,
        "unique_attribute": "arn",
    },
    "list_endpoints": {
        "input_token": "next_token",
        "limit_key": "max_results",
        "limit_default": 100,
        "unique_attribute": "arn",
    },
    "list_endpoint_configs": {
        "input_token": "next_token",
        "limit_key": "max_results",
        "limit_default": 100,
        "unique_attribute": "arn",
    },
    "list_compilation_jobs": {
        "input_token": "next_token",
        "limit_key": "max_results",
        "limit_default": 100,
        "unique_attribute": "arn",
    },
    "list_domains": {
        "input_token": "next_token",
        "limit_key": "max_results",
        "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",
    },
    "list_hyper_parameter_tuning_jobs": {
        "input_token": "next_token",
        "limit_key": "max_results",
        "limit_default": 100,
        "unique_attribute": "arn",
    },
    "list_model_quality_job_definitions": {
        "input_token": "next_token",
        "limit_key": "max_results",
        "limit_default": 100,
        "unique_attribute": "arn",
    },
    "list_model_cards": {
        "input_token": "next_token",
        "limit_key": "max_results",
        "limit_default": 100,
        "unique_attribute": "arn",
    },
    "list_model_card_versions": {
        "input_token": "next_token",
        "limit_key": "max_results",
        "limit_default": 100,
        "unique_attribute": "model_card_arn",
    },
    "list_model_bias_job_definitions": {
        "input_token": "next_token",
        "limit_key": "max_results",
        "limit_default": 100,
        "unique_attribute": "arn",
    },
    "list_data_quality_job_definitions": {
        "input_token": "next_token",
        "limit_key": "max_results",
        "limit_default": 100,
        "unique_attribute": "arn",
    },
}

METRIC_INFO_TYPE = dict[str, str | int | float | datetime]
METRIC_STEP_TYPE = dict[int, METRIC_INFO_TYPE]


class BaseObject(BaseModel):
    def camelCase(self, key: str) -> str:
        words = []
        for word in key.split("_"):
            words.append(word.title())
        return "".join(words)

    def update(self, details_json: str) -> None:
        details = json.loads(details_json)
        for k in details.keys():
            setattr(self, k, details[k])

    def gen_response_object(self) -> dict[str, Any]:
        response_object: dict[str, Any] = {}
        for key, value in self.__dict__.items():
            if "_" in key:
                response_object[self.camelCase(key)] = value
            else:
                response_object[key[0].upper() + key[1:]] = value
        return response_object

    @property
    def response_object(self) -> dict[str, Any]:  # type: ignore[misc]
        return self.gen_response_object()


class FakePipelineExecution(BaseObject):
    def __init__(
        self,
        pipeline_execution_arn: str,
        pipeline_execution_display_name: str,
        pipeline_parameters: list[dict[str, str]],
        pipeline_execution_description: str,
        parallelism_configuration: dict[str, int],
        pipeline_definition: str,
        client_request_token: str,
    ):
        self.arn = pipeline_execution_arn
        self.pipeline_execution_display_name = pipeline_execution_display_name
        self.pipeline_parameters = pipeline_parameters
        self.pipeline_execution_description = pipeline_execution_description
        self.pipeline_execution_status = "Succeeded"
        self.pipeline_execution_failure_reason = None
        self.parallelism_configuration = parallelism_configuration
        self.pipeline_definition_for_execution = pipeline_definition
        self.client_request_token = client_request_token

        now = utcnow()
        self.creation_time = now
        self.last_modified_time = now
        self.start_time = now

        fake_user_profile_name = "fake-user-profile-name"
        fake_domain_id = "fake-domain-id"
        fake_user_profile_arn = arn_formatter(
            "user-profile",
            f"{fake_domain_id}/{fake_user_profile_name}",
            pipeline_execution_arn.split(":")[4],
            pipeline_execution_arn.split(":")[3],
        )
        self.created_by = {
            "UserProfileArn": fake_user_profile_arn,
            "UserProfileName": fake_user_profile_name,
            "DomainId": fake_domain_id,
        }
        self.last_modified_by = {
            "UserProfileArn": fake_user_profile_arn,
            "UserProfileName": fake_user_profile_name,
            "DomainId": fake_domain_id,
        }


class FakePipeline(BaseObject):
    def __init__(
        self,
        pipeline_name: str,
        pipeline_display_name: str,
        pipeline_definition: str,
        pipeline_description: str,
        role_arn: str,
        tags: list[dict[str, str]],
        account_id: str,
        region_name: str,
        parallelism_configuration: dict[str, int],
    ):
        self.pipeline_name = pipeline_name
        self.arn = arn_formatter("pipeline", pipeline_name, account_id, region_name)
        self.pipeline_display_name = pipeline_display_name or pipeline_name
        self.pipeline_definition = pipeline_definition
        self.pipeline_description = pipeline_description
        self.pipeline_executions: dict[str, FakePipelineExecution] = {}
        self.role_arn = role_arn
        self.tags = tags or []
        self.parallelism_configuration = parallelism_configuration

        now = utcnow()
        self.creation_time = now
        self.last_modified_time = now
        self.last_execution_time: datetime | None = None

        self.pipeline_status = "Active"
        fake_user_profile_name = "fake-user-profile-name"
        fake_domain_id = "fake-domain-id"
        fake_user_profile_arn = arn_formatter(
            "user-profile",
            f"{fake_domain_id}/{fake_user_profile_name}",
            account_id,
            region_name,
        )
        self.created_by = {
            "UserProfileArn": fake_user_profile_arn,
            "UserProfileName": fake_user_profile_name,
            "DomainId": fake_domain_id,
        }
        self.last_modified_by = {
            "UserProfileArn": fake_user_profile_arn,
            "UserProfileName": fake_user_profile_name,
            "DomainId": fake_domain_id,
        }


class FakeProcessingJob(BaseObject):
    def __init__(
        self,
        app_specification: dict[str, Any],
        experiment_config: dict[str, str],
        network_config: dict[str, Any],
        processing_inputs: list[dict[str, Any]],
        processing_job_name: str,
        processing_output_config: dict[str, Any],
        account_id: str,
        region_name: str,
        role_arn: str,
        tags: list[dict[str, str]],
        stopping_condition: dict[str, int],
    ):
        self.processing_job_name = processing_job_name
        self.arn = FakeProcessingJob.arn_formatter(
            processing_job_name, account_id, region_name
        )
        now = utcnow()
        self.creation_time = now
        self.last_modified_time = now
        self.processing_end_time = now
        self.tags = tags or []
        self.role_arn = role_arn
        self.app_specification = app_specification
        self.experiment_config = experiment_config
        self.network_config = network_config
        self.processing_inputs = processing_inputs
        self.processing_job_status = "Completed"
        self.processing_output_config = processing_output_config
        self.stopping_condition = stopping_condition

    @property
    def response_object(self) -> dict[str, Any]:  # type: ignore[misc]
        response_object = self.gen_response_object()
        response = {
            k: v for k, v in response_object.items() if v is not None and v != [None]
        }
        response["ProcessingJobArn"] = response.pop("Arn")

        return response

    @property
    def response_create(self) -> dict[str, str]:
        return {"ProcessingJobArn": self.arn}

    @staticmethod
    def arn_formatter(name: str, account_id: str, region: str) -> str:
        return arn_formatter("processing-job", name, account_id, region)


class FakeTrainingJob(BaseObject):
    def __init__(
        self,
        account_id: str,
        region_name: str,
        training_job_name: str,
        hyper_parameters: dict[str, str],
        algorithm_specification: dict[str, Any],
        role_arn: str,
        input_data_config: list[dict[str, Any]],
        output_data_config: dict[str, str],
        resource_config: dict[str, Any],
        vpc_config: dict[str, list[str]],
        stopping_condition: dict[str, int],
        tags: list[dict[str, str]],
        enable_network_isolation: bool,
        enable_inter_container_traffic_encryption: bool,
        enable_managed_spot_training: bool,
        checkpoint_config: dict[str, str],
        debug_hook_config: dict[str, Any],
        debug_rule_configurations: list[dict[str, Any]],
        tensor_board_output_config: dict[str, str],
        experiment_config: dict[str, str],
    ):
        self.training_job_name = training_job_name
        self.hyper_parameters = hyper_parameters
        self.algorithm_specification = algorithm_specification
        self.role_arn = role_arn
        self.input_data_config = input_data_config
        self.output_data_config = output_data_config
        self.resource_config = resource_config
        self.vpc_config = vpc_config
        self.stopping_condition = stopping_condition
        self.tags = tags or []
        self.enable_network_isolation = enable_network_isolation
        self.enable_inter_container_traffic_encryption = (
            enable_inter_container_traffic_encryption
        )
        self.enable_managed_spot_training = enable_managed_spot_training
        self.checkpoint_config = checkpoint_config
        self.debug_hook_config = debug_hook_config
        self.debug_rule_configurations = debug_rule_configurations
        self.tensor_board_output_config = tensor_board_output_config
        self.experiment_config = experiment_config
        self.arn = FakeTrainingJob.arn_formatter(
            training_job_name, account_id, region_name
        )
        self.creation_time = self.last_modified_time = utcnow()
        self.model_artifacts = {
            "S3ModelArtifacts": os.path.join(
                self.output_data_config["S3OutputPath"],
                self.training_job_name,
                "output",
                "model.tar.gz",
            )
        }
        self.training_job_status = "Completed"
        self.secondary_status = "Completed"
        self.algorithm_specification["MetricDefinitions"] = [
            {
                "Name": "test:dcg",
                "Regex": "#quality_metric: host=\\S+, test dcg <score>=(\\S+)",
            }
        ]
        now = utcnow()
        self.creation_time = now
        self.last_modified_time = now
        self.training_start_time = now
        self.training_end_time = now
        self.secondary_status_transitions = [
            {
                "Status": "Starting",
                "StartTime": self.creation_time,
                "EndTime": self.creation_time,
                "StatusMessage": "Preparing the instances for training",
            }
        ]
        self.final_metric_data_list = [
            {
                "MetricName": "train:progress",
                "Value": 100.0,
                "Timestamp": self.creation_time,
            }
        ]

    @property
    def response_object(self) -> dict[str, Any]:  # type: ignore[misc]
        response_object = self.gen_response_object()
        response = {
            k: v for k, v in response_object.items() if v is not None and v != [None]
        }
        response["TrainingJobArn"] = response.pop("Arn")

        return response

    @property
    def response_create(self) -> dict[str, str]:
        return {"TrainingJobArn": self.arn}

    @staticmethod
    def arn_formatter(name: str, account_id: str, region_name: str) -> str:
        return arn_formatter("training-job", name, account_id, region_name)


class FakeEndpoint(BaseObject, CloudFormationModel):
    def __init__(
        self,
        account_id: str,
        region_name: str,
        endpoint_name: str,
        endpoint_config_name: str,
        production_variants: list[dict[str, Any]],
        data_capture_config: dict[str, Any],
        tags: list[dict[str, str]],
    ):
        self.endpoint_name = endpoint_name
        self.arn = FakeEndpoint.arn_formatter(endpoint_name, account_id, region_name)
        self.endpoint_config_name = endpoint_config_name
        self.production_variants = self._process_production_variants(
            production_variants
        )
        self.data_capture_config = data_capture_config
        self.tags = tags or []
        self.endpoint_status = "InService"
        self.failure_reason = None
        self.creation_time = self.last_modified_time = utcnow()

    def _process_production_variants(
        self, production_variants: list[dict[str, Any]]
    ) -> list[dict[str, Any]]:
        endpoint_variants = []
        for production_variant in production_variants:
            temp_variant = {}

            # VariantName is the only required param
            temp_variant["VariantName"] = production_variant["VariantName"]

            if production_variant.get("InitialInstanceCount", None):
                temp_variant["CurrentInstanceCount"] = production_variant[
                    "InitialInstanceCount"
                ]
                temp_variant["DesiredInstanceCount"] = production_variant[
                    "InitialInstanceCount"
                ]

            if production_variant.get("InitialVariantWeight", None):
                temp_variant["CurrentWeight"] = production_variant[
                    "InitialVariantWeight"
                ]
                temp_variant["DesiredWeight"] = production_variant[
                    "InitialVariantWeight"
                ]

            if production_variant.get("ServerlessConfig", None):
                temp_variant["CurrentServerlessConfig"] = production_variant[
                    "ServerlessConfig"
                ]
                temp_variant["DesiredServerlessConfig"] = production_variant[
                    "ServerlessConfig"
                ]

            endpoint_variants.append(temp_variant)

        return endpoint_variants

    def summary(self) -> dict[str, Any]:
        return {
            "EndpointName": self.endpoint_name,
            "EndpointArn": self.arn,
            "CreationTime": self.creation_time,
            "LastModifiedTime": self.last_modified_time,
            "EndpointStatus": self.endpoint_status,
        }

    @property
    def response_object(self) -> dict[str, Any]:  # type: ignore[misc]
        response_object = self.gen_response_object()
        response = {
            k: v for k, v in response_object.items() if v is not None and v != [None]
        }
        response["EndpointArn"] = response.pop("Arn")

        return response

    @property
    def response_create(self) -> dict[str, str]:
        return {"EndpointArn": self.arn}

    @staticmethod
    def arn_formatter(endpoint_name: str, account_id: str, region_name: str) -> str:
        return arn_formatter("endpoint", endpoint_name, account_id, region_name)

    @property
    def physical_resource_id(self) -> str:
        return self.arn

    @classmethod
    def has_cfn_attr(cls, attr: str) -> bool:
        return attr in ["EndpointName"]

    def get_cfn_attribute(self, attribute_name: str) -> str:
        # https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-sagemaker-endpoint.html#aws-resource-sagemaker-endpoint-return-values
        from moto.cloudformation.exceptions import UnformattedGetAttTemplateException

        if attribute_name == "EndpointName":
            return self.endpoint_name
        raise UnformattedGetAttTemplateException()

    @staticmethod
    def cloudformation_name_type() -> str:
        return ""

    @staticmethod
    def cloudformation_type() -> str:
        # https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-sagemaker-endpoint.html
        return "AWS::SageMaker::Endpoint"

    @classmethod
    def create_from_cloudformation_json(  # type: ignore[misc]
        cls,
        resource_name: str,
        cloudformation_json: Any,
        account_id: str,
        region_name: str,
        **kwargs: Any,
    ) -> "FakeEndpoint":
        sagemaker_backend = sagemaker_backends[account_id][region_name]

        # Get required properties from provided CloudFormation template
        properties = cloudformation_json["Properties"]
        endpoint_config_name = properties["EndpointConfigName"]

        endpoint = sagemaker_backend.create_endpoint(
            endpoint_name=resource_name,
            endpoint_config_name=endpoint_config_name,
            tags=properties.get("Tags", []),
        )
        return endpoint

    @classmethod
    def update_from_cloudformation_json(  # type: ignore[misc]
        cls,
        original_resource: Any,
        new_resource_name: str,
        cloudformation_json: Any,
        account_id: str,
        region_name: str,
    ) -> "FakeEndpoint":
        # Changes to the Endpoint will not change resource name
        cls.delete_from_cloudformation_json(
            original_resource.arn, cloudformation_json, account_id, region_name
        )
        new_resource = cls.create_from_cloudformation_json(
            original_resource.endpoint_name,
            cloudformation_json,
            account_id,
            region_name,
        )
        return new_resource

    @classmethod
    def delete_from_cloudformation_json(  # type: ignore[misc]
        cls,
        resource_name: str,
        cloudformation_json: Any,
        account_id: str,
        region_name: str,
    ) -> None:
        # Get actual name because resource_name actually provides the ARN
        # since the Physical Resource ID is the ARN despite SageMaker
        # using the name for most of its operations.
        endpoint_name = resource_name.split("/")[-1]

        sagemaker_backends[account_id][region_name].delete_endpoint(endpoint_name)


class FakeEndpointConfig(BaseObject, CloudFormationModel):
    def __init__(
        self,
        account_id: str,
        region_name: str,
        endpoint_config_name: str,
        production_variants: list[dict[str, Any]],
        async_inference_config: dict[str, Any],
        data_capture_config: dict[str, Any],
        tags: list[dict[str, Any]],
        kms_key_id: str,
    ):
        self.validate_production_variants(production_variants)

        self.endpoint_config_name = endpoint_config_name
        self.endpoint_config_arn = FakeEndpointConfig.arn_formatter(
            endpoint_config_name, account_id, region_name
        )
        self.arn = (self.endpoint_config_arn,)
        self.production_variants = production_variants or []
        self.async_inference_config = async_inference_config
        self.data_capture_config = data_capture_config or {}
        self.tags = tags or []
        self.kms_key_id = kms_key_id
        self.creation_time = utcnow()

    def validate_production_variants(
        self, production_variants: list[dict[str, Any]]
    ) -> None:
        for production_variant in production_variants:
            if "InstanceType" in production_variant.keys():
                self.validate_instance_type(production_variant["InstanceType"])
            elif "ServerlessConfig" in production_variant.keys():
                self.validate_serverless_config(production_variant["ServerlessConfig"])
            else:
                message = f"Invalid Keys for ProductionVariant: received {production_variant.keys()} but expected it to contain one of {['InstanceType', 'ServerlessConfig']}"
                raise ValidationError(message=message)

    def validate_serverless_config(self, serverless_config: dict[str, Any]) -> None:
        VALID_SERVERLESS_MEMORY_SIZE = [1024, 2048, 3072, 4096, 5120, 6144]
        if not validators.is_one_of(
            serverless_config["MemorySizeInMB"], VALID_SERVERLESS_MEMORY_SIZE
        ):
            message = f"Value '{serverless_config['MemorySizeInMB']}' at 'MemorySizeInMB' failed to satisfy constraint: Member must satisfy enum value set: {VALID_SERVERLESS_MEMORY_SIZE}"
            raise ValidationError(message=message)

    def validate_instance_type(self, instance_type: str) -> None:
        VALID_INSTANCE_TYPES = [
            "ml.r5d.12xlarge",
            "ml.r5.12xlarge",
            "ml.p2.xlarge",
            "ml.m5.4xlarge",
            "ml.m4.16xlarge",
            "ml.r5d.24xlarge",
            "ml.r5.24xlarge",
            "ml.p3.16xlarge",
            "ml.m5d.xlarge",
            "ml.m5.large",
            "ml.t2.xlarge",
            "ml.p2.16xlarge",
            "ml.m5d.12xlarge",
            "ml.inf1.2xlarge",
            "ml.m5d.24xlarge",
            "ml.c4.2xlarge",
            "ml.c5.2xlarge",
            "ml.c4.4xlarge",
            "ml.inf1.6xlarge",
            "ml.c5d.2xlarge",
            "ml.c5.4xlarge",
            "ml.g4dn.xlarge",
            "ml.g4dn.12xlarge",
            "ml.c5d.4xlarge",
            "ml.g4dn.2xlarge",
            "ml.c4.8xlarge",
            "ml.c4.large",
            "ml.c5d.xlarge",
            "ml.c5.large",
            "ml.g4dn.4xlarge",
            "ml.c5.9xlarge",
            "ml.g4dn.16xlarge",
            "ml.c5d.large",
            "ml.c5.xlarge",
            "ml.c5d.9xlarge",
            "ml.c4.xlarge",
            "ml.inf1.xlarge",
            "ml.g4dn.8xlarge",
            "ml.inf1.24xlarge",
            "ml.m5d.2xlarge",
            "ml.t2.2xlarge",
            "ml.c5d.18xlarge",
            "ml.m5d.4xlarge",
            "ml.t2.medium",
            "ml.c5.18xlarge",
            "ml.r5d.2xlarge",
            "ml.r5.2xlarge",
            "ml.p3.2xlarge",
            "ml.m5d.large",
            "ml.m5.xlarge",
            "ml.m4.10xlarge",
            "ml.t2.large",
            "ml.r5d.4xlarge",
            "ml.r5.4xlarge",
            "ml.m5.12xlarge",
            "ml.m4.xlarge",
            "ml.m5.24xlarge",
            "ml.m4.2xlarge",
            "ml.p2.8xlarge",
            "ml.m5.2xlarge",
            "ml.r5d.xlarge",
            "ml.r5d.large",
            "ml.r5.xlarge",
            "ml.r5.large",
            "ml.p3.8xlarge",
            "ml.m4.4xlarge",
        ]
        if not validators.is_one_of(instance_type, VALID_INSTANCE_TYPES):
            message = f"Value '{instance_type}' at 'instanceType' failed to satisfy constraint: Member must satisfy enum value set: {VALID_INSTANCE_TYPES}"
            raise ValidationError(message=message)

    def summary(self) -> dict[str, Any]:
        return {
            "EndpointConfigName": self.endpoint_config_name,
            "EndpointConfigArn": self.endpoint_config_arn,
            "CreationTime": self.creation_time,
        }

    @property
    def response_object(self) -> dict[str, Any]:  # type: ignore[misc]
        response_object = self.gen_response_object()
        return {
            k: v for k, v in response_object.items() if v is not None and v != [None]
        }

    @property
    def response_create(self) -> dict[str, str]:
        return {"EndpointConfigArn": self.endpoint_config_arn}

    @staticmethod
    def arn_formatter(
        endpoint_config_name: str, account_id: str, region_name: str
    ) -> str:
        return arn_formatter(
            "endpoint-config", endpoint_config_name, account_id, region_name
        )

    @property
    def physical_resource_id(self) -> str:
        return self.endpoint_config_arn

    @classmethod
    def has_cfn_attr(cls, attr: str) -> bool:
        return attr in ["EndpointConfigName"]

    def get_cfn_attribute(self, attribute_name: str) -> str:
        # https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-sagemaker-endpointconfig.html#aws-resource-sagemaker-endpointconfig-return-values
        from moto.cloudformation.exceptions import UnformattedGetAttTemplateException

        if attribute_name == "EndpointConfigName":
            return self.endpoint_config_name
        raise UnformattedGetAttTemplateException()

    @staticmethod
    def cloudformation_name_type() -> str:
        return ""

    @staticmethod
    def cloudformation_type() -> str:
        # https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-sagemaker-endpointconfig.html
        return "AWS::SageMaker::EndpointConfig"

    @classmethod
    def create_from_cloudformation_json(  # type: ignore[misc]
        cls,
        resource_name: str,
        cloudformation_json: Any,
        account_id: str,
        region_name: str,
        **kwargs: Any,
    ) -> "FakeEndpointConfig":
        sagemaker_backend = sagemaker_backends[account_id][region_name]

        # Get required properties from provided CloudFormation template
        properties = cloudformation_json["Properties"]
        production_variants = properties["ProductionVariants"]

        endpoint_config = sagemaker_backend.create_endpoint_config(
            endpoint_config_name=resource_name,
            production_variants=production_variants,
            data_capture_config=properties.get("DataCaptureConfig", {}),
            async_inference_config=properties.get("AsyncInferenceConfig", {}),
            kms_key_id=properties.get("KmsKeyId"),
            tags=properties.get("Tags", []),
        )
        return endpoint_config

    @classmethod
    def update_from_cloudformation_json(  # type: ignore[misc]
        cls,
        original_resource: Any,
        new_resource_name: str,
        cloudformation_json: Any,
        account_id: str,
        region_name: str,
    ) -> "FakeEndpointConfig":
        # Most changes to the endpoint config will change resource name for EndpointConfigs
        cls.delete_from_cloudformation_json(
            original_resource.endpoint_config_arn,
            cloudformation_json,
            account_id,
            region_name,
        )
        new_resource = cls.create_from_cloudformation_json(
            new_resource_name, cloudformation_json, account_id, region_name
        )
        return new_resource

    @classmethod
    def delete_from_cloudformation_json(  # type: ignore[misc]
        cls,
        resource_name: str,
        cloudformation_json: Any,
        account_id: str,
        region_name: str,
    ) -> None:
        # Get actual name because resource_name actually provides the ARN
        # since the Physical Resource ID is the ARN despite SageMaker
        # using the name for most of its operations.
        endpoint_config_name = resource_name.split("/")[-1]

        sagemaker_backends[account_id][region_name].delete_endpoint_config(
            endpoint_config_name
        )


class FakeTransformJob(BaseObject):
    def __init__(
        self,
        account_id: str,
        region_name: str,
        transform_job_name: str,
        model_name: str,
        max_concurrent_transforms: int,
        model_client_config: dict[str, int],
        max_payload_in_mb: int,
        batch_strategy: str,
        environment: dict[str, str],
        transform_input: dict[str, dict[str, str] | str],
        transform_output: dict[str, str],
        data_capture_config: dict[str, str | bool],
        transform_resources: dict[str, str | int],
        data_processing: dict[str, str],
        tags: dict[str, str],
        experiment_config: dict[str, str],
    ):
        self.transform_job_name = transform_job_name
        self.model_name = model_name
        self.max_concurrent_transforms = max_concurrent_transforms
        self.model_client_config = model_client_config
        self.max_payload_in_mb = max_payload_in_mb
        self.batch_strategy = batch_strategy
        self.environment = environment
        self.transform_input = transform_input
        self.transform_output = transform_output
        self.data_capture_config = data_capture_config
        self.transform_resources = transform_resources
        self.data_processing = data_processing
        self.tags = tags
        self.experiment_config = experiment_config
        self.arn = FakeTransformJob.arn_formatter(
            transform_job_name, account_id, region_name
        )
        self.transform_job_status = "Completed"
        self.failure_reason = ""
        self.labeling_job_arn = ""
        self.auto_ml_job_arn = ""
        now = utcnow()
        self.creation_time = now
        self.transform_start_time = now
        self.transform_end_time = now
        self.last_modified_time = now

    # Override title case
    def camelCase(self, key: str) -> str:
        words = []
        for word in key.split("_"):
            if word == "mb":
                words.append("MB")
            else:
                words.append(word.title())
        return "".join(words)

    @property
    def response_object(self) -> dict[str, Any]:  # type: ignore[misc]
        response_object = self.gen_response_object()
        response = {
            k: v for k, v in response_object.items() if v is not None and v != [None]
        }
        response["TransformJobArn"] = response.pop("Arn")
        return response

    @property
    def response_create(self) -> dict[str, str]:
        return {"TransformJobArn": self.arn}

    @staticmethod
    def arn_formatter(name: str, account_id: str, region_name: str) -> str:
        return arn_formatter("transform-job", name, account_id, region_name)


class Model(BaseObject, CloudFormationModel):
    def __init__(
        self,
        account_id: str,
        region_name: str,
        model_name: str,
        execution_role_arn: str,
        primary_container: dict[str, Any],
        vpc_config: dict[str, Any],
        containers: list[dict[str, Any]] | None = None,
        tags: list[dict[str, str]] | None = None,
    ):
        self.model_name = model_name
        self.creation_time = utcnow()
        self.containers = containers or []
        self.tags = tags or []
        self.enable_network_isolation = False
        self.vpc_config = vpc_config
        self.primary_container = primary_container
        self.execution_role_arn = execution_role_arn or "arn:test"
        self.arn = arn_formatter("model", self.model_name, account_id, region_name)

    @property
    def response_object(self) -> dict[str, Any]:  # type: ignore[misc]
        response_object = self.gen_response_object()
        response = {
            k: v for k, v in response_object.items() if v is not None and v != [None]
        }
        response["ModelArn"] = response.pop("Arn")

        return response

    @property
    def response_create(self) -> dict[str, str]:
        return {"ModelArn": self.arn}

    @property
    def physical_resource_id(self) -> str:
        return self.arn

    @classmethod
    def has_cfn_attr(cls, attr: str) -> bool:
        return attr in ["ModelName"]

    def get_cfn_attribute(self, attribute_name: str) -> str:
        # https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-sagemaker-model.html#aws-resource-sagemaker-model-return-values
        from moto.cloudformation.exceptions import UnformattedGetAttTemplateException

        if attribute_name == "ModelName":
            return self.model_name
        raise UnformattedGetAttTemplateException()

    @staticmethod
    def cloudformation_name_type() -> str:
        return ""

    @staticmethod
    def cloudformation_type() -> str:
        # https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-sagemaker-model.html
        return "AWS::SageMaker::Model"

    @classmethod
    def create_from_cloudformation_json(  # type: ignore[misc]
        cls,
        resource_name: str,
        cloudformation_json: Any,
        account_id: str,
        region_name: str,
        **kwargs: Any,
    ) -> "Model":
        sagemaker_backend = sagemaker_backends[account_id][region_name]

        # Get required properties from provided CloudFormation template
        properties = cloudformation_json["Properties"]
        execution_role_arn = properties["ExecutionRoleArn"]
        primary_container = properties["PrimaryContainer"]

        model = sagemaker_backend.create_model(
            model_name=resource_name,
            execution_role_arn=execution_role_arn,
            primary_container=primary_container,
            vpc_config=properties.get("VpcConfig", {}),
            containers=properties.get("Containers", []),
            tags=properties.get("Tags", []),
        )
        return model

    @classmethod
    def update_from_cloudformation_json(  # type: ignore[misc]
        cls,
        original_resource: Any,
        new_resource_name: str,
        cloudformation_json: Any,
        account_id: str,
        region_name: str,
    ) -> "Model":
        # Most changes to the model will change resource name for Models
        cls.delete_from_cloudformation_json(
            original_resource.arn, cloudformation_json, account_id, region_name
        )
        new_resource = cls.create_from_cloudformation_json(
            new_resource_name, cloudformation_json, account_id, region_name
        )
        return new_resource

    @classmethod
    def delete_from_cloudformation_json(  # type: ignore[misc]
        cls,
        resource_name: str,
        cloudformation_json: Any,
        account_id: str,
        region_name: str,
    ) -> None:
        # Get actual name because resource_name actually provides the ARN
        # since the Physical Resource ID is the ARN despite SageMaker
        # using the name for most of its operations.
        model_name = resource_name.split("/")[-1]

        sagemaker_backends[account_id][region_name].delete_model(model_name)


class ModelPackageGroup(BaseObject):
    def __init__(
        self,
        model_package_group_name: str,
        model_package_group_description: str,
        account_id: str,
        region_name: str,
        tags: list[dict[str, str]] | None = None,
    ) -> None:
        model_package_group_arn = arn_formatter(
            region_name=region_name,
            account_id=account_id,
            _type="model-package-group",
            _id=model_package_group_name,
        )
        fake_user_profile_name = "fake-user-profile-name"
        fake_domain_id = "fake-domain-id"
        fake_user_profile_arn = arn_formatter(
            _type="user-profile",
            _id=f"{fake_domain_id}/{fake_user_profile_name}",
            account_id=account_id,
            region_name=region_name,
        )
        datetime_now = utcnow()
        self.model_package_group_name = model_package_group_name
        self.arn = model_package_group_arn
        self.model_package_group_description = model_package_group_description
        self.creation_time = datetime_now
        self.created_by = {
            "UserProfileArn": fake_user_profile_arn,
            "UserProfileName": fake_user_profile_name,
            "DomainId": fake_domain_id,
        }
        self.model_package_group_status = "Completed"
        self.tags = tags

    def gen_response_object(self) -> dict[str, Any]:
        response_object = super().gen_response_object()
        response_values = [
            "ModelPackageGroupName",
            "Arn",
            "ModelPackageGroupDescription",
            "CreationTime",
            "ModelPackageGroupStatus",
            "Tags",
        ]
        response = {k: v for k, v in response_object.items() if k in response_values}
        response["ModelPackageGroupArn"] = response.pop("Arn")

        return response


class FakeModelCard(BaseObject):
    def __init__(
        self,
        account_id: str,
        region_name: str,
        model_card_name: str,
        model_card_version: int,
        content: str,
        model_card_status: str,
        security_config: dict[str, str] | None = None,
        tags: list[dict[str, Any]] | None = None,
        creation_time: datetime | None = None,
        last_modified_time: datetime | None = None,
    ) -> None:
        datetime_now = utcnow()
        self.arn = arn_formatter("model-card", model_card_name, account_id, region_name)
        self.model_card_name = model_card_name
        self.model_card_version = model_card_version
        self.content = content
        self.model_card_status = model_card_status
        self.creation_time = creation_time if creation_time else datetime_now
        self.last_modified_time = (
            last_modified_time if last_modified_time else datetime_now
        )
        self.security_config = security_config
        self.tags = tags

    def describe(self) -> dict[str, Any]:
        return {
            "ModelCardArn": self.arn,
            "ModelCardName": self.model_card_name,
            "ModelCardVersion": self.model_card_version,
            "Content": self.content,
            "ModelCardStatus": self.model_card_status,
            "SecurityConfig": self.security_config,
            "CreationTime": self.creation_time,
            "CreatedBy": {},
            "LastModifiedTime": self.creation_time,
            "LastModifiedBy": {},
        }

    def summary(self) -> dict[str, Any]:
        return {
            "ModelCardName": self.model_card_name,
            "ModelCardArn": self.arn,
            "ModelCardStatus": self.model_card_status,
            "CreationTime": self.creation_time,
            "LastModifiedTime": self.last_modified_time,
        }

    def version_summary(self) -> dict[str, Any]:
        return {
            "ModelCardName": self.model_card_name,
            "ModelCardArn": self.arn,
            "ModelCardStatus": self.model_card_status,
            "ModelCardVersion": self.model_card_version,
            "CreationTime": self.creation_time,
            "LastModifiedTime": self.last_modified_time,
        }


class FeatureGroup(BaseObject):
    def __init__(
        self,
        region_name: str,
        account_id: str,
        feature_group_name: str,
        record_identifier_feature_name: str,
        event_time_feature_name: str,
        feature_definitions: list[dict[str, str]],
        offline_store_config: dict[str, Any],
        role_arn: str,
        tags: list[dict[str, str]] | None = None,
    ) -> None:
        self.feature_group_name = feature_group_name
        self.record_identifier_feature_name = record_identifier_feature_name
        self.event_time_feature_name = event_time_feature_name
        self.feature_definitions = feature_definitions

        table_name = (
            f"{feature_group_name.replace('-', '_')}_{int(utcnow().timestamp())}"
        )
        offline_store_config["DataCatalogConfig"] = {
            "TableName": table_name,
            "Catalog": "AwsDataCatalog",
            "Database": "sagemaker_featurestore",
        }
        offline_store_config["S3StorageConfig"]["ResolvedOutputS3Uri"] = (
            f"{offline_store_config['S3StorageConfig']['S3Uri']}/{account_id}/{region_name}/offline-store/{feature_group_name}-{int(utcnow().timestamp())}/data"
        )

        self.offline_store_config = offline_store_config
        self.role_arn = role_arn

        self.creation_time = utcnow()
        self.arn = arn_formatter(
            region_name=region_name,
            account_id=account_id,
            _type="feature-group",
            _id=f"{self.feature_group_name.lower()}",
        )
        self.tags = tags

    def describe(self) -> dict[str, Any]:
        return {
            "FeatureGroupArn": self.arn,
            "FeatureGroupName": self.feature_group_name,
            "RecordIdentifierFeatureName": self.record_identifier_feature_name,
            "EventTimeFeatureName": self.event_time_feature_name,
            "FeatureDefinitions": self.feature_definitions,
            "CreationTime": self.creation_time,
            "OfflineStoreConfig": self.offline_store_config,
            "RoleArn": self.role_arn,
            "ThroughputConfig": {"ThroughputMode": "OnDemand"},
            "FeatureGroupStatus": "Created",
        }


class ModelPackage(BaseObject):
    def __init__(
        self,
        model_package_name: str,
        model_package_group_name: str | None,
        model_package_version: int | None,
        model_package_description: str | None,
        inference_specification: Any,
        source_algorithm_specification: Any,
        validation_specification: Any,
        certify_for_marketplace: bool,
        model_approval_status: str | None,
        metadata_properties: Any,
        model_metrics: Any,
        approval_description: str | None,
        customer_metadata_properties: Any,
        drift_check_baselines: Any,
        domain: str,
        task: str,
        sample_payload_url: str,
        additional_inference_specifications: list[Any],
        client_token: str,
        region_name: str,
        account_id: str,
        model_package_type: str,
        tags: list[dict[str, str]] | None = None,
    ) -> None:
        fake_user_profile_name = "fake-user-profile-name"
        fake_domain_id = "fake-domain-id"
        fake_user_profile_arn = arn_formatter(
            _type="user-profile",
            _id=f"{fake_domain_id}/{fake_user_profile_name}",
            account_id=account_id,
            region_name=region_name,
        )
        model_package_arn = arn_formatter(
            region_name=region_name,
            account_id=account_id,
            _type="model-package",
            _id=(
                f"{model_package_name.lower()}/{model_package_version}"
                if model_package_version
                else model_package_name.lower()
            ),
        )
        datetime_now = utcnow()
        self.model_package_name = model_package_name
        self.model_package_group_name = model_package_group_name
        self.model_package_version = model_package_version
        self.arn = model_package_arn
        self.model_package_description = model_package_description
        self.creation_time = datetime_now
        self.inference_specification = inference_specification
        self.source_algorithm_specification = source_algorithm_specification
        self.validation_specification = validation_specification
        self.model_package_type = model_package_type
        self.model_package_status_details = {
            "ValidationStatuses": [
                {
                    "Name": model_package_arn,
                    "Status": "Completed",
                }
            ],
            "ImageScanStatuses": [
                {
                    "Name": model_package_arn,
                    "Status": "Completed",
                }
            ],
        }
        self.certify_for_marketplace = certify_for_marketplace
        self.model_approval_status: str | None = None
        self.set_model_approval_status(model_approval_status)
        self.created_by = {
            "UserProfileArn": fake_user_profile_arn,
            "UserProfileName": fake_user_profile_name,
            "DomainId": fake_domain_id,
        }
        self.metadata_properties = metadata_properties
        self.model_metrics = model_metrics
        self.last_modified_time: datetime | None = None
        self.approval_description = approval_description
        self.customer_metadata_properties = customer_metadata_properties
        self.drift_check_baselines = drift_check_baselines
        self.domain = domain
        self.task = task
        self.sample_payload_url = sample_payload_url
        self.additional_inference_specifications: list[Any] | None = None
        self.add_additional_inference_specifications(
            additional_inference_specifications
        )
        self.tags = tags
        self.model_package_status = "Completed"
        self.last_modified_by: dict[str, str] | None = None
        self.client_token = client_token

    def gen_response_object(self) -> dict[str, Any]:
        response_object = super().gen_response_object()
        response_values = [
            "ModelPackageName",
            "ModelPackageGroupName",
            "ModelPackageVersion",
            "Arn",
            "ModelPackageDescription",
            "CreationTime",
            "InferenceSpecification",
            "SourceAlgorithmSpecification",
            "ValidationSpecification",
            "ModelPackageStatus",
            "ModelPackageStatusDetails",
            "CertifyForMarketplace",
            "ModelApprovalStatus",
            "CreatedBy",
            "MetadataProperties",
            "ModelMetrics",
            "LastModifiedTime",
            "LastModifiedBy",
            "ApprovalDescription",
            "CustomerMetadataProperties",
            "DriftCheckBaselines",
            "Domain",
            "Task",
            "SamplePayloadUrl",
            "AdditionalInferenceSpecifications",
            "SkipModelValidation",
        ]
        if self.model_package_type == "Versioned":
            del response_object["ModelPackageName"]
        elif self.model_package_type == "Unversioned":
            del response_object["ModelPackageGroupName"]
        response = {
            k: v
            for k, v in response_object.items()
            if k in response_values
            if v is not None
        }
        response["ModelPackageArn"] = response.pop("Arn")

        return response

    def modifications_done(self) -> None:
        self.last_modified_time = utcnow()
        self.last_modified_by = self.created_by

    def set_model_approval_status(self, model_approval_status: str | None) -> None:
        if model_approval_status is not None:
            validate_model_approval_status(model_approval_status)
        self.model_approval_status = model_approval_status

    def remove_customer_metadata_property(
        self, customer_metadata_properties_to_remove: list[str]
    ) -> None:
        if customer_metadata_properties_to_remove is not None:
            for customer_metadata_property in customer_metadata_properties_to_remove:
                self.customer_metadata_properties.pop(customer_metadata_property, None)

    def add_additional_inference_specifications(
        self, additional_inference_specifications_to_add: list[Any] | None
    ) -> None:
        self.validate_additional_inference_specifications(
            additional_inference_specifications_to_add
        )
        if (
            self.additional_inference_specifications is not None
            and additional_inference_specifications_to_add is not None
        ):
            self.additional_inference_specifications.extend(
                additional_inference_specifications_to_add
            )
        else:
            self.additional_inference_specifications = (
                additional_inference_specifications_to_add
            )

    def validate_additional_inference_specifications(
        self, additional_inference_specifications: list[dict[str, Any]] | None
    ) -> None:
        specifications_to_validate = additional_inference_specifications or []
        for additional_inference_specification in specifications_to_validate:
            if "SupportedTransformInstanceTypes" in additional_inference_specification:
                self.validate_supported_transform_instance_types(
                    additional_inference_specification[
                        "SupportedTransformInstanceTypes"
                    ]
                )
            if (
                "SupportedRealtimeInferenceInstanceTypes"
                in additional_inference_specification
            ):
                self.validate_supported_realtime_inference_instance_types(
                    additional_inference_specification[
                        "SupportedRealtimeInferenceInstanceTypes"
                    ]
                )

    @staticmethod
    def validate_supported_transform_instance_types(instance_types: list[str]) -> None:
        VALID_TRANSFORM_INSTANCE_TYPES = [
            "ml.m4.xlarge",
            "ml.m4.2xlarge",
            "ml.m4.4xlarge",
            "ml.m4.10xlarge",
            "ml.m4.16xlarge",
            "ml.c4.xlarge",
            "ml.c4.2xlarge",
            "ml.c4.4xlarge",
            "ml.c4.8xlarge",
            "ml.p2.xlarge",
            "ml.p2.8xlarge",
            "ml.p2.16xlarge",
            "ml.p3.2xlarge",
            "ml.p3.8xlarge",
            "ml.p3.16xlarge",
            "ml.c5.xlarge",
            "ml.c5.2xlarge",
            "ml.c5.4xlarge",
            "ml.c5.9xlarge",
            "ml.c5.18xlarge",
            "ml.m5.large",
            "ml.m5.xlarge",
            "ml.m5.2xlarge",
            "ml.m5.4xlarge",
            "ml.m5.12xlarge",
            "ml.m5.24xlarge",
            "ml.g4dn.xlarge",
            "ml.g4dn.2xlarge",
            "ml.g4dn.4xlarge",
            "ml.g4dn.8xlarge",
            "ml.g4dn.12xlarge",
            "ml.g4dn.16xlarge",
        ]
        for instance_type in instance_types:
            if not validators.is_one_of(instance_type, VALID_TRANSFORM_INSTANCE_TYPES):
                message = f"Value '{instance_type}' at 'SupportedTransformInstanceTypes' failed to satisfy constraint: Member must satisfy enum value set: {VALID_TRANSFORM_INSTANCE_TYPES}"
                raise ValidationError(message=message)

    @staticmethod
    def validate_supported_realtime_inference_instance_types(
        instance_types: list[str],
    ) -> None:
        VALID_REALTIME_INFERENCE_INSTANCE_TYPES = [
            "ml.t2.medium",
            "ml.t2.large",
            "ml.t2.xlarge",
            "ml.t2.2xlarge",
            "ml.m4.xlarge",
            "ml.m4.2xlarge",
            "ml.m4.4xlarge",
            "ml.m4.10xlarge",
            "ml.m4.16xlarge",
            "ml.m5.large",
            "ml.m5.xlarge",
            "ml.m5.2xlarge",
            "ml.m5.4xlarge",
            "ml.m5.12xlarge",
            "ml.m5.24xlarge",
            "ml.m5d.large",
            "ml.m5d.xlarge",
            "ml.m5d.2xlarge",
            "ml.m5d.4xlarge",
            "ml.m5d.12xlarge",
            "ml.m5d.24xlarge",
            "ml.c4.large",
            "ml.c4.xlarge",
            "ml.c4.2xlarge",
            "ml.c4.4xlarge",
            "ml.c4.8xlarge",
            "ml.p2.xlarge",
            "ml.p2.8xlarge",
            "ml.p2.16xlarge",
            "ml.p3.2xlarge",
            "ml.p3.8xlarge",
            "ml.p3.16xlarge",
            "ml.c5.large",
            "ml.c5.xlarge",
            "ml.c5.2xlarge",
            "ml.c5.4xlarge",
            "ml.c5.9xlarge",
            "ml.c5.18xlarge",
            "ml.c5d.large",
            "ml.c5d.xlarge",
            "ml.c5d.2xlarge",
            "ml.c5d.4xlarge",
            "ml.c5d.9xlarge",
            "ml.c5d.18xlarge",
            "ml.g4dn.xlarge",
            "ml.g4dn.2xlarge",
            "ml.g4dn.4xlarge",
            "ml.g4dn.8xlarge",
            "ml.g4dn.12xlarge",
            "ml.g4dn.16xlarge",
            "ml.r5.large",
            "ml.r5.xlarge",
            "ml.r5.2xlarge",
            "ml.r5.4xlarge",
            "ml.r5.12xlarge",
            "ml.r5.24xlarge",
            "ml.r5d.large",
            "ml.r5d.xlarge",
            "ml.r5d.2xlarge",
            "ml.r5d.4xlarge",
            "ml.r5d.12xlarge",
            "ml.r5d.24xlarge",
            "ml.inf1.xlarge",
            "ml.inf1.2xlarge",
            "ml.inf1.6xlarge",
            "ml.inf1.24xlarge",
            "ml.c6i.large",
            "ml.c6i.xlarge",
            "ml.c6i.2xlarge",
            "ml.c6i.4xlarge",
            "ml.c6i.8xlarge",
            "ml.c6i.12xlarge",
            "ml.c6i.16xlarge",
            "ml.c6i.24xlarge",
            "ml.c6i.32xlarge",
            "ml.g5.xlarge",
            "ml.g5.2xlarge",
            "ml.g5.4xlarge",
            "ml.g5.8xlarge",
            "ml.g5.12xlarge",
            "ml.g5.16xlarge",
            "ml.g5.24xlarge",
            "ml.g5.48xlarge",
            "ml.p4d.24xlarge",
            "ml.c7g.large",
            "ml.c7g.xlarge",
            "ml.c7g.2xlarge",
            "ml.c7g.4xlarge",
            "ml.c7g.8xlarge",
            "ml.c7g.12xlarge",
            "ml.c7g.16xlarge",
            "ml.m6g.large",
            "ml.m6g.xlarge",
            "ml.m6g.2xlarge",
            "ml.m6g.4xlarge",
            "ml.m6g.8xlarge",
            "ml.m6g.12xlarge",
            "ml.m6g.16xlarge",
            "ml.m6gd.large",
            "ml.m6gd.xlarge",
            "ml.m6gd.2xlarge",
            "ml.m6gd.4xlarge",
            "ml.m6gd.8xlarge",
            "ml.m6gd.12xlarge",
            "ml.m6gd.16xlarge",
            "ml.c6g.large",
            "ml.c6g.xlarge",
            "ml.c6g.2xlarge",
            "ml.c6g.4xlarge",
            "ml.c6g.8xlarge",
            "ml.c6g.12xlarge",
            "ml.c6g.16xlarge",
            "ml.c6gd.large",
            "ml.c6gd.xlarge",
            "ml.c6gd.2xlarge",
            "ml.c6gd.4xlarge",
            "ml.c6gd.8xlarge",
            "ml.c6gd.12xlarge",
            "ml.c6gd.16xlarge",
            "ml.c6gn.large",
            "ml.c6gn.xlarge",
            "ml.c6gn.2xlarge",
            "ml.c6gn.4xlarge",
            "ml.c6gn.8xlarge",
            "ml.c6gn.12xlarge",
            "ml.c6gn.16xlarge",
            "ml.r6g.large",
            "ml.r6g.xlarge",
            "ml.r6g.2xlarge",
            "ml.r6g.4xlarge",
            "ml.r6g.8xlarge",
            "ml.r6g.12xlarge",
            "ml.r6g.16xlarge",
            "ml.r6gd.large",
            "ml.r6gd.xlarge",
            "ml.r6gd.2xlarge",
            "ml.r6gd.4xlarge",
            "ml.r6gd.8xlarge",
            "ml.r6gd.12xlarge",
            "ml.r6gd.16xlarge",
            "ml.p4de.24xlarge",
            "ml.trn1.2xlarge",
            "ml.trn1.32xlarge",
            "ml.inf2.xlarge",
            "ml.inf2.8xlarge",
            "ml.inf2.24xlarge",
            "ml.inf2.48xlarge",
            "ml.p5.48xlarge",
        ]
        for instance_type in instance_types:
            if not validators.is_one_of(
                instance_type, VALID_REALTIME_INFERENCE_INSTANCE_TYPES
            ):
                message = f"Value '{instance_type}' at 'SupportedRealtimeInferenceInstanceTypes' failed to satisfy constraint: Member must satisfy enum value set: {VALID_REALTIME_INFERENCE_INSTANCE_TYPES}"
                raise ValidationError(message=message)


class Cluster(BaseObject):
    def __init__(
        self,
        cluster_name: str,
        region_name: str,
        account_id: str,
        instance_groups: list[dict[str, Any]],
        vpc_config: dict[str, list[str]],
        tags: list[dict[str, str]] | None = None,
    ):
        self.region_name = region_name
        self.account_id = account_id
        self.cluster_name = cluster_name
        if cluster_name in sagemaker_backends[account_id][region_name].clusters:
            raise ResourceInUseException(
                message=f"Resource Already Exists: Cluster with name {cluster_name} already exists. Choose a different name."
            )
        self.instance_groups = instance_groups
        for instance_group in instance_groups:
            self.valid_cluster_node_instance_types(instance_group["InstanceType"])
            if not instance_group["LifeCycleConfig"]["SourceS3Uri"].startswith(
                "s3://sagemaker-"
            ):
                raise ValidationError(
                    message=f"Validation Error: SourceS3Uri {instance_group['LifeCycleConfig']['SourceS3Uri']} does not start with 's3://sagemaker'."
                )
        self.vpc_config = vpc_config
        self.tags = tags or []
        self.arn = arn_formatter("cluster", self.cluster_name, account_id, region_name)
        self.status = "InService"
        self.creation_time = utcnow()
        self.failure_message = ""
        self.nodes: dict[str, ClusterNode] = {}
        for instance_group in self.instance_groups:
            instance_group["CurrentCount"] = instance_group["InstanceCount"]
            instance_group["TargetCount"] = instance_group["InstanceCount"]
            del instance_group["InstanceCount"]

    def describe(self) -> dict[str, Any]:
        return {
            "ClusterArn": self.arn,
            "ClusterName": self.cluster_name,
            "ClusterStatus": self.status,
            "CreationTime": self.creation_time,
            "FailureMessage": self.failure_message,
            "InstanceGroups": self.instance_groups,
            "VpcConfig": self.vpc_config,
        }

    def summary(self) -> dict[str, Any]:
        return {
            "ClusterArn": self.arn,
            "ClusterName": self.cluster_name,
            "CreationTime": self.creation_time,
            "ClusterStatus": self.status,
        }

    def valid_cluster_node_instance_types(self, instance_type: str) -> None:
        VALID_CLUSTER_INSTANCE_TYPES = [
            "ml.p4d.24xlarge",
            "ml.p4de.24xlarge",
            "ml.p5.48xlarge",
            "ml.trn1.32xlarge",
            "ml.trn1n.32xlarge",
            "ml.g5.xlarge",
            "ml.g5.2xlarge",
            "ml.g5.4xlarge",
            "ml.g5.8xlarge",
            "ml.g5.12xlarge",
            "ml.g5.16xlarge",
            "ml.g5.24xlarge",
            "ml.g5.48xlarge",
            "ml.c5.large",
            "ml.c5.xlarge",
            "ml.c5.2xlarge",
            "ml.c5.4xlarge",
            "ml.c5.9xlarge",
            "ml.c5.12xlarge",
            "ml.c5.18xlarge",
            "ml.c5.24xlarge",
            "ml.c5n.large",
            "ml.c5n.2xlarge",
            "ml.c5n.4xlarge",
            "ml.c5n.9xlarge",
            "ml.c5n.18xlarge",
            "ml.m5.large",
            "ml.m5.xlarge",
            "ml.m5.2xlarge",
            "ml.m5.4xlarge",
            "ml.m5.8xlarge",
            "ml.m5.12xlarge",
            "ml.m5.16xlarge",
            "ml.m5.24xlarge",
            "ml.t3.medium",
            "ml.t3.large",
            "ml.t3.xlarge",
            "ml.t3.2xlarge",
        ]

        if instance_type not in VALID_CLUSTER_INSTANCE_TYPES:
            message = f"Value '{instance_type}' at 'InstanceType' failed to satisfy constraint: Member must satisfy enum value set: {VALID_CLUSTER_INSTANCE_TYPES}"
            raise ValidationError(message=message)


class ClusterNode(BaseObject):
    def __init__(
        self,
        region_name: str,
        account_id: str,
        cluster_name: str,
        instance_group_name: str,
        instance_type: str,
        life_cycle_config: dict[str, Any],
        execution_role: str,
        node_id: str,
        threads_per_core: int | None = None,
    ):
        self.region_name = region_name
        self.account_id = account_id
        self.cluster_name = cluster_name
        self.instance_group_name = (
            instance_group_name  # probably need to do something with this
        )
        self.instance_id = node_id  # generate instance id
        self.instance_type = instance_type
        self.life_cycle_config = life_cycle_config
        self.execution_role = execution_role
        self.threads_per_core = threads_per_core
        self.status = "Running"
        self.launch_time = utcnow()

    def describe(self) -> dict[str, Any]:
        return {
            "InstanceGroupName": self.instance_group_name,
            "InstanceId": self.instance_id,
            "InstanceStatus": {"Status": self.status, "Message": "message"},
            "InstanceType": self.instance_type,
            "LaunchTime": self.launch_time,
            "LifeCycleConfig": self.life_cycle_config,
            "ThreadsPerCore": self.threads_per_core,
        }

    def summary(self) -> dict[str, Any]:
        return {
            "InstanceGroupName": self.instance_group_name,
            "InstanceId": self.instance_id,
            "InstanceType": self.instance_type,
            "LaunchTime": self.launch_time,
            "InstanceStatus": {"Status": self.status, "Message": "message"},
        }


class CompilationJob(BaseObject):
    def __init__(
        self,
        compilation_job_name: str,
        role_arn: str,
        region_name: str,
        account_id: str,
        output_config: dict[str, Any],
        stopping_condition: dict[str, Any],
        model_package_version_arn: str | None,
        input_config: dict[str, Any] | None,
        vpc_config: dict[str, Any] | None,
        tags: list[dict[str, str]] | None,
    ):
        self.compilation_job_name = compilation_job_name
        if (
            compilation_job_name
            in sagemaker_backends[account_id][region_name].compilation_jobs
        ):
            raise ResourceInUseException(
                message=f"Resource Already Exists: Compilation job with name {compilation_job_name} already exists. Choose a different name."
            )
        self.arn = arn_formatter(
            "compilation-job", self.compilation_job_name, account_id, region_name
        )
        self.compilation_job_status = "COMPLETED"
        self.compilation_start_time = utcnow()
        self.compilation_end_time = utcnow()
        self.stopping_condition = stopping_condition
        self.inference_image = "InferenceImage"
        self.model_package_version_arn = model_package_version_arn
        self.creation_time = utcnow()
        self.last_modified_time = utcnow()
        self.failure_reason = ""
        self.model_artifacts = {"S3ModelArtifacts": output_config["S3OutputLocation"]}
        self.model_digests = {
            "ArtifactDigest": "786a02f742015903c6c6fd852552d272912f4740e15847618a86e217f71f5419d25e1031afee585313896444934eb04b903a685b1448b755d56f701afe9be2ce"
        }
        self.role_arn = role_arn
        self.input_config = input_config
        if input_config and model_package_version_arn:
            raise ValidationError(
                message="InputConfig and ModelPackageVersionArn cannot be specified at the same time."
            )
        if not input_config and not model_package_version_arn:
            raise ValidationError(
                message="Either InputConfig or ModelPackageVersionArn must be specified."
            )
        self.output_config = output_config
        self.vpc_config = vpc_config
        self.derived_information = {"DerivedDataInputConfig": "DerivedDataInputConfig"}
        self.tags = tags

    def describe(self) -> dict[str, Any]:
        return {
            "CompilationJobName": self.compilation_job_name,
            "CompilationJobArn": self.arn,
            "CompilationJobStatus": self.compilation_job_status,
            "CompilationStartTime": self.compilation_start_time,
            "CompilationEndTime": self.compilation_end_time,
            "StoppingCondition": self.stopping_condition,
            "InferenceImage": self.inference_image,
            "ModelPackageVersionArn": self.model_package_version_arn,
            "CreationTime": self.creation_time,
            "LastModifiedTime": self.last_modified_time,
            "FailureReason": self.failure_reason,
            "ModelArtifacts": self.model_artifacts,
            "ModelDigests": self.model_digests,
            "RoleArn": self.role_arn,
            "InputConfig": self.input_config,
            "OutputConfig": self.output_config,
            "VpcConfig": self.vpc_config,
            "DerivedInformation": self.derived_information,
        }

    def summary(self) -> dict[str, Any]:
        summary = {
            "CompilationJobName": self.compilation_job_name,
            "CompilationJobArn": self.arn,
            "CreationTime": self.creation_time,
            "CompilationStartTime": self.compilation_start_time,
            "CompilationEndTime": self.compilation_end_time,
            "LastModifiedTime": self.last_modified_time,
            "CompilationJobStatus": self.compilation_job_status,
        }
        if "TargetDevice" in self.output_config:
            summary["CompilationTargetDevice"] = self.output_config["TargetDevice"]
        else:
            summary["CompilationTargetPlatformOs"] = self.output_config[
                "TargetPlatform"
            ]["Os"]
            summary["CompilationTargetPlatformArch"] = self.output_config[
                "TargetPlatform"
            ]["Arch"]
            summary["CompilationTargetPlatformAccelerator"] = self.output_config[
                "TargetPlatform"
            ]["Accelerator"]
        return summary


class AutoMLJob(BaseObject):
    def __init__(
        self,
        auto_ml_job_name: str,
        auto_ml_job_input_data_config: list[dict[str, Any]],
        output_data_config: dict[str, Any],
        auto_ml_problem_type_config: dict[str, Any],
        role_arn: str,
        region_name: str,
        account_id: str,
        security_config: dict[str, Any] | None,
        auto_ml_job_objective: dict[str, Any] | None,
        model_deploy_config: dict[str, Any] | None,
        data_split_config: dict[str, Any] | None,
        tags: list[dict[str, str]] | None = None,
    ):
        self.region_name = region_name
        self.account_id = account_id
        self.auto_ml_job_name = auto_ml_job_name
        if auto_ml_job_name in sagemaker_backends[account_id][region_name].auto_ml_jobs:
            raise ResourceInUseException(
                message=f"Resource Already Exists: Auto ML Job with name {auto_ml_job_name} already exists. Choose a different name."
            )
        self.auto_ml_job_input_data_config = auto_ml_job_input_data_config
        self.output_data_config = output_data_config
        self.auto_ml_problem_type_config = auto_ml_problem_type_config
        self.role_arn = role_arn
        self.security_config = security_config
        self.auto_ml_job_objective = auto_ml_job_objective
        self.auto_ml_problem_type_resolved_attributes: dict[str, Any] | None = None
        if "ImageClassificationJobConfig" in self.auto_ml_problem_type_config:
            self.auto_ml_job_objective = (
                {"MetricName": "Accuracy"}
                if self.auto_ml_job_objective is None
                else self.auto_ml_job_objective
            )
            self.auto_ml_problem_type_config_name = "ImageClassification"
        elif "TextClassificationJobConfig" in self.auto_ml_problem_type_config:
            self.auto_ml_job_objective = (
                {"MetricName": "Accuracy"}
                if self.auto_ml_job_objective is None
                else self.auto_ml_job_objective
            )
            self.auto_ml_problem_type_config_name = "TextClassification"
        elif "TimeSeriesForecastingJobConfig" in self.auto_ml_problem_type_config:
            self.auto_ml_job_objective = (
                {"MetricName": "AverageWeightedQuantileLoss"}
                if self.auto_ml_job_objective is None
                else self.auto_ml_job_objective
            )
            self.auto_ml_problem_type_config_name = "TimeSeriesForecasting"
        elif "TabularJobConfig" in self.auto_ml_problem_type_config:
            self.auto_ml_problem_type_config_name = "Tabular"
            if (
                self.auto_ml_problem_type_config["TabularJobConfig"]["ProblemType"]
                == "BinaryClassification"
            ):
                self.auto_ml_job_objective = (
                    {"MetricName": "F1"}
                    if self.auto_ml_job_objective is None
                    else self.auto_ml_job_objective
                )
                self.auto_ml_problem_type_resolved_attributes = {
                    "TabularResolvedAttributes": {
                        "TabularProblemType": "BinaryClassification"
                    }
                }
            if (
                self.auto_ml_problem_type_config["TabularJobConfig"]["ProblemType"]
                == "MulticlassClassification"
            ):
                self.auto_ml_job_objective = (
                    {"MetricName": "Accuracy"}
                    if self.auto_ml_job_objective is None
                    else self.auto_ml_job_objective
                )
                self.auto_ml_problem_type_resolved_attributes = {
                    "TabularResolvedAttributes": {
                        "TabularProblemType": "MulticlassClassification"
                    }
                }
            if (
                self.auto_ml_problem_type_config["TabularJobConfig"]["ProblemType"]
                == "Regression"
            ):
                self.auto_ml_job_objective = (
                    {"MetricName": "MSE"}
                    if self.auto_ml_job_objective is None
                    else self.auto_ml_job_objective
                )
                self.auto_ml_problem_type_resolved_attributes = {
                    "TabularResolvedAttributes": {"TabularProblemType": "Regression"}
                }
        elif "TextGenerationJobConfig" in self.auto_ml_problem_type_config:
            self.auto_ml_problem_type_config_name = "TextGeneration"
            self.auto_ml_job_objective = (
                {"MetricName": ""}
                if self.auto_ml_job_objective is None
                else self.auto_ml_job_objective
            )
            self.auto_ml_problem_type_resolved_attributes = {
                "TextGenerationResolvedAttributes": {"BaseModelName": "string"}
            }

        self.model_deploy_config = (
            model_deploy_config
            if model_deploy_config
            else {"AutoGenerateEndpointName": False, "EndpointName": "EndpointName"}
        )
        if (
            "AutoGenerateEndpointName" in self.model_deploy_config
            and self.model_deploy_config["AutoGenerateEndpointName"]
            and "EndpointName" in self.model_deploy_config
        ):
            raise ValidationError(
                message="Validation Error: An EndpointName cannot be provided while AutoGenerateEndpoint name is True."
            )
        self.output_data_config = output_data_config
        self.data_split_config = (
            data_split_config if data_split_config else {"ValidationFraction": 0.2}
        )
        self.tags = tags or []
        self.arn = arn_formatter(
            "automl-job", self.auto_ml_job_name, account_id, region_name
        )
        self.creation_time = utcnow()
        self.end_time = utcnow()
        self.last_modified_time = utcnow()
        self.failure_reason = ""
        self.partial_failure_reasons = [{"PartialFailureMessage": ""}]
        self.best_candidate = {
            "CandidateName": "best_candidate",
            "FinalAutoMLJobObjectiveMetric": {
                "Type": "Maximize",
                "MetricName": "Accuracy",
                "Value": 123,
                "StandardMetricName": "Accuracy",
            },
            "ObjectiveStatus": "Succeeded",
            "CandidateSteps": [
                {
                    "CandidateStepType": "AWS::SageMaker::TrainingJob",
                    "CandidateStepArn": arn_formatter(
                        "training-job", "candidate_step_name", account_id, region_name
                    ),
                    "CandidateStepName": "candidate_step_name",
                },
            ],
            "CandidateStatus": "Completed",
            "InferenceContainers": [
                {
                    "Image": "string",
                    "ModelDataUrl": "string",
                    "Environment": {"string": "string"},
                },
            ],
            "CreationTime": datetime(2024, 1, 1),
            "EndTime": datetime(2024, 1, 1),
            "LastModifiedTime": datetime(2024, 1, 1),
            "FailureReason": "string",
            "CandidateProperties": {
                "CandidateArtifactLocations": {
                    "Explainability": "string",
                    "ModelInsights": "string",
                    "BacktestResults": "string",
                },
                "CandidateMetrics": [
                    {
                        "MetricName": "Accuracy",
                        "Value": 123,
                        "Set": "Train",
                        "StandardMetricName": "Accuracy",
                    },
                ],
            },
            "InferenceContainerDefinitions": {
                "string": [
                    {
                        "Image": "string",
                        "ModelDataUrl": "string",
                        "Environment": {"string": "string"},
                    },
                ]
            },
        }
        self.auto_ml_job_status = "InProgress"
        self.auto_ml_job_secondary_status = "Completed"
        self.auto_ml_job_artifacts = {
            "CandidateDefinitionNotebookLocation": "candidate/notebook/location",
            "DataExplorationNotebookLocation": "data/notebook/location",
        }

        resolved = {
            "AutoMLJobObjective": self.auto_ml_job_objective,
            "CompletionCriteria": self.auto_ml_problem_type_config[
                self.auto_ml_problem_type_config_name + "JobConfig"
            ]["CompletionCriteria"],
        }
        if self.auto_ml_problem_type_resolved_attributes is not None:
            resolved["AutoMLProblemTypeResolvedAttributes"] = (
                self.auto_ml_problem_type_resolved_attributes
            )
        self.resolved_attributes = resolved

        self.model_deploy_result = {
            "EndpointName": (
                self.model_deploy_config["EndpointName"]
                if self.model_deploy_config
                else "endpoint_name"
            ),
        }

    def describe(self) -> dict[str, Any]:
        return {
            "AutoMLJobName": self.auto_ml_job_name,
            "AutoMLJobArn": self.arn,
            "AutoMLJobInputDataConfig": self.auto_ml_job_input_data_config,
            "OutputDataConfig": self.output_data_config,
            "RoleArn": self.role_arn,
            "AutoMLJobObjective": self.auto_ml_job_objective,
            "AutoMLProblemTypeConfig": self.auto_ml_problem_type_config,
            "AutoMLProblemTypeConfigName": self.auto_ml_problem_type_config_name,
            "CreationTime": self.creation_time,
            "EndTime": self.end_time,
            "LastModifiedTime": self.last_modified_time,
            "FailureReason": self.failure_reason,
            "PartialFailureReasons": self.partial_failure_reasons,
            "BestCandidate": self.best_candidate,
            "AutoMLJobStatus": self.auto_ml_job_status,
            "AutoMLJobSecondaryStatus": self.auto_ml_job_secondary_status,
            "AutoMLJobArtifacts": self.auto_ml_job_artifacts,
            "ResolvedAttributes": self.resolved_attributes,
            "ModelDeployConfig": self.model_deploy_config,
            "ModelDeployResult": self.model_deploy_result,
            "DataSplitConfig": self.data_split_config,
            "SecurityConfig": self.security_config,
        }

    def summary(self) -> dict[str, Any]:
        return {
            "AutoMLJobName": self.auto_ml_job_name,
            "AutoMLJobArn": self.arn,
            "AutoMLJobStatus": self.auto_ml_job_status,
            "AutoMLJobSecondaryStatus": self.auto_ml_job_secondary_status,
            "CreationTime": self.creation_time,
            "EndTime": self.end_time,
            "LastModifiedTime": self.last_modified_time,
            "FailureReason": self.failure_reason,
            "PartialFailureReasons": self.partial_failure_reasons,
        }


class Domain(BaseObject):
    def __init__(
        self,
        domain_name: str,
        auth_mode: str,
        default_user_settings: dict[str, Any],
        subnet_ids: list[str],
        vpc_id: str,
        account_id: str,
        region_name: str,
        domain_settings: dict[str, Any] | None,
        tags: list[dict[str, str]] | None,
        app_network_access_type: str | None,
        home_efs_file_system_kms_key_id: str | None,
        kms_key_id: str | None,
        app_security_group_management: str | None,
        default_space_settings: dict[str, Any] | None,
    ):
        self.domain_name = domain_name
        if domain_name in sagemaker_backends[account_id][region_name].domains:
            raise ResourceInUseException(
                message=f"Resource Already Exists: Domain with name {domain_name} already exists. Choose a different name."
            )
        self.auth_mode = auth_mode
        self.default_user_settings = default_user_settings
        self.subnet_ids = subnet_ids
        self.vpc_id = vpc_id
        self.account_id = account_id
        self.region_name = region_name
        self.domain_settings = domain_settings
        self.tags = tags
        self.app_network_access_type = (
            app_network_access_type if app_network_access_type else "PublicInternetOnly"
        )
        self.home_efs_file_system_kms_key_id = (
            home_efs_file_system_kms_key_id
            if home_efs_file_system_kms_key_id
            else kms_key_id
        )
        self.kms_key_id = kms_key_id
        self.app_security_group_management = app_security_group_management
        self.default_space_settings = default_space_settings

        self.id = f"d-{domain_name}"
        self.arn = arn_formatter("domain", self.id, account_id, region_name)
        self.home_efs_file_system_id = f"{domain_name}-efs-id"
        self.single_sign_on_managed_application_instance_id = f"{domain_name}-sso-id"
        self.single_sign_on_managed_application_arn = arn_formatter(
            "sso", f"application/{domain_name}/apl-{domain_name}", account_id, ""
        )
        self.status = "InService"
        self.creation_time = utcnow()
        self.last_modified_time = utcnow()
        self.failure_reason = ""
        self.security_group_id_for_domain_boundary = f"sg-{domain_name}"
        self.url = f"{domain_name}.{region_name}.sagemaker.test.com"

    def describe(self) -> dict[str, Any]:
        return {
            "DomainArn": self.arn,
            "DomainId": self.id,
            "DomainName": self.domain_name,
            "HomeEfsFileSystemId": self.home_efs_file_system_id,
            "SingleSignOnManagedApplicationInstanceId": self.single_sign_on_managed_application_instance_id,
            "SingleSignOnApplicationArn": self.single_sign_on_managed_application_arn,
            "Status": self.status,
            "CreationTime": self.creation_time,
            "LastModifiedTime": self.last_modified_time,
            "FailureReason": self.failure_reason,
            "SecurityGroupIdForDomainBoundary": self.security_group_id_for_domain_boundary,
            "AuthMode": self.auth_mode,
            "DefaultUserSettings": self.default_user_settings,
            "DomainSettings": self.domain_settings,
            "AppNetworkAccessType": self.app_network_access_type,
            "HomeEfsFileSystemKmsKeyId": self.home_efs_file_system_kms_key_id,
            "SubnetIds": self.subnet_ids,
            "Url": self.url,
            "VpcId": self.vpc_id,
            "KmsKeyId": self.kms_key_id,
            "AppSecurityGroupManagement": self.app_security_group_management,
            "DefaultSpaceSettings": self.default_space_settings,
        }

    def summary(self) -> dict[str, Any]:
        return {
            "DomainArn": self.arn,
            "DomainId": self.id,
            "DomainName": self.domain_name,
            "Status": self.status,
            "CreationTime": self.creation_time,
            "LastModifiedTime": self.last_modified_time,
            "Url": self.url,
        }


class ModelExplainabilityJobDefinition(BaseObject):
    def __init__(
        self,
        job_definition_name: str,
        model_explainability_baseline_config: dict[str, Any] | None,
        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: dict[str, Any] | None,
        role_arn: str,
        stopping_condition: dict[str, Any] | None,
        region_name: str,
        account_id: str,
        tags: list[dict[str, str]] | None,
    ):
        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 = utcnow()
        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 HyperParameterTuningJob(BaseObject):
    def __init__(
        self,
        hyper_parameter_tuning_job_name: str,
        hyper_parameter_tuning_job_config: dict[str, Any],
        region_name: str,
        account_id: str,
        training_job_definition: dict[str, Any] | None,
        training_job_definitions: list[dict[str, Any]] | None,
        warm_start_config: dict[str, Any] | None,
        tags: list[dict[str, str]] | None,
        autotune: dict[str, Any] | None,
    ):
        self.hyper_parameter_tuning_job_name = hyper_parameter_tuning_job_name
        if (
            hyper_parameter_tuning_job_name
            in sagemaker_backends[account_id][region_name].hyper_parameter_tuning_jobs
        ):
            raise ResourceInUseException(
                message=f"Resource Already Exists: Hyper Parameter Tuning Job with name {hyper_parameter_tuning_job_name} already exists. Choose a different name."
            )
        self.arn = arn_formatter(
            "hyper-parameter-tuning-job",
            self.hyper_parameter_tuning_job_name,
            account_id,
            region_name,
        )
        self.hyper_parameter_tuning_job_config = hyper_parameter_tuning_job_config
        self.region_name = region_name
        self.account_id = account_id
        self.training_job_definition = training_job_definition
        self.training_job_definitions = training_job_definitions
        self.hyper_parameter_tuning_job_status = "Completed"
        self.creation_time = utcnow()
        self.last_modified_time = utcnow()
        self.hyper_parameter_tuning_end_time = utcnow()
        self.training_job_status_counters = {
            "Completed": 1,
            "InProgress": 0,
            "RetryableError": 0,
            "NonRetryableError": 0,
            "Stopped": 0,
        }
        self.objective_status_counters = {
            "Succeeded": 1,
            "Pending": 0,
            "Failed": 0,
        }
        self.best_training_job = {
            "TrainingJobDefinitionName": "string",
            "TrainingJobName": "FakeTrainingJobName",
            "TrainingJobArn": "FakeTrainingJobArn",
            "TuningJobName": "FakeTuningJobName",
            "CreationTime": datetime(2024, 1, 1),
            "TrainingStartTime": datetime(2024, 1, 1),
            "TrainingEndTime": datetime(2024, 1, 1),
            "TrainingJobStatus": "Completed",
            "TunedHyperParameters": {"string": "TunedHyperParameters"},
            "FailureReason": "string",
            "FinalHyperParameterTuningJobObjectiveMetric": {
                "Type": "Maximize",
                "MetricName": "Accuracy",
                "Value": 1,
            },
            "ObjectiveStatus": "Succeeded",
        }
        self.OverallBestTrainingJob = {
            "TrainingJobDefinitionName": "FakeTrainingJobDefinitionName",
            "TrainingJobName": "FakeTrainingJobName",
            "TrainingJobArn": "FakeTrainingJobArn",
            "TuningJobName": "FakeTuningJobName",
            "CreationTime": datetime(2024, 1, 1),
            "TrainingStartTime": datetime(2024, 1, 1),
            "TrainingEndTime": datetime(2024, 1, 1),
            "TrainingJobStatus": "Completed",
            "TunedHyperParameters": {"string": "FakeTunedHyperParameters"},
            "FailureReason": "FakeFailureReason",
            "FinalHyperParameterTuningJobObjectiveMetric": {
                "Type": "Maximize",
                "MetricName": "Acccuracy",
                "Value": 1,
            },
            "ObjectiveStatus": "Succeeded",
        }
        self.warm_start_config = warm_start_config
        self.failure_reason = ""
        self.tuning_job_completion_details = {
            "NumberOfTrainingJobsObjectiveNotImproving": 123,
            "ConvergenceDetectedTime": datetime(2024, 1, 1),
        }
        self.consumed_resources = {"RuntimeInSeconds": 123}
        self.tags = tags
        self.autotune = autotune

    def describe(self) -> dict[str, Any]:
        return {
            "HyperParameterTuningJobName": self.hyper_parameter_tuning_job_name,
            "HyperParameterTuningJobArn": self.arn,
            "HyperParameterTuningJobConfig": self.hyper_parameter_tuning_job_config,
            "TrainingJobDefinition": self.training_job_definition,
            "TrainingJobDefinitions": self.training_job_definitions,
            "HyperParameterTuningJobStatus": self.hyper_parameter_tuning_job_status,
            "CreationTime": self.creation_time,
            "HyperParameterTuningEndTime": self.hyper_parameter_tuning_end_time,
            "LastModifiedTime": self.last_modified_time,
            "TrainingJobStatusCounters": self.training_job_status_counters,
            "ObjectiveStatusCounters": self.objective_status_counters,
            "BestTrainingJob": self.best_training_job,
            "OverallBestTrainingJob": self.OverallBestTrainingJob,
            "WarmStartConfig": self.warm_start_config,
            "Autotune": self.autotune,
            "FailureReason": self.failure_reason,
            "TuningJobCompletionDetails": self.tuning_job_completion_details,
            "ConsumedResources": self.consumed_resources,
        }

    def summary(self) -> dict[str, Any]:
        return {
            "HyperParameterTuningJobName": self.hyper_parameter_tuning_job_name,
            "HyperParameterTuningJobArn": self.arn,
            "HyperParameterTuningJobStatus": self.hyper_parameter_tuning_job_status,
            "Strategy": self.hyper_parameter_tuning_job_config["Strategy"],
            "CreationTime": self.creation_time,
            "HyperParameterTuningEndTime": self.hyper_parameter_tuning_end_time,
            "LastModifiedTime": self.last_modified_time,
            "TrainingJobStatusCounters": self.training_job_status_counters,
            "ObjectiveStatusCounters": self.objective_status_counters,
            "ResourceLimits": self.hyper_parameter_tuning_job_config["ResourceLimits"],
        }


class ModelQualityJobDefinition(BaseObject):
    def __init__(
        self,
        job_definition_name: str,
        model_quality_baseline_config: dict[str, Any] | None,
        model_quality_app_specification: dict[str, Any],
        model_quality_job_input: dict[str, Any],
        model_quality_job_output_config: dict[str, Any],
        job_resources: dict[str, Any],
        network_config: dict[str, Any] | None,
        role_arn: str,
        stopping_condition: dict[str, Any] | None,
        tags: list[dict[str, str]] | None,
        region_name: str,
        account_id: str,
    ):
        self.region_name = region_name
        self.account_id = account_id
        self.job_definition_name = job_definition_name
        if (
            job_definition_name
            in sagemaker_backends[account_id][region_name].model_quality_job_definitions
        ):
            raise ResourceInUseException(
                message=f"Resource Already Exists: Model Quality Job Definition with name {job_definition_name} already exists. Choose a different name."
            )
        self.model_quality_baseline_config = model_quality_baseline_config
        self.model_quality_app_specification = model_quality_app_specification
        self.model_quality_job_input = model_quality_job_input
        self.model_quality_job_output_config = model_quality_job_output_config
        self.job_resources = job_resources
        self.network_config = network_config
        self.role_arn = role_arn
        self.stopping_condition = stopping_condition
        self.tags = tags or []
        self.arn = arn_formatter(
            "model-quality-job-definition",
            self.job_definition_name,
            account_id,
            region_name,
        )
        self.creation_time = utcnow()
        self.endpoint_name = self.model_quality_job_input["EndpointInput"][
            "EndpointName"
        ]

    def describe(self) -> dict[str, Any]:
        return {
            "JobDefinitionArn": self.arn,
            "JobDefinitionName": self.job_definition_name,
            "CreationTime": self.creation_time,
            "ModelQualityBaselineConfig": self.model_quality_baseline_config,
            "ModelQualityAppSpecification": self.model_quality_app_specification,
            "ModelQualityJobInput": self.model_quality_job_input,
            "ModelQualityJobOutputConfig": self.model_quality_job_output_config,
            "JobResources": self.job_resources,
            "NetworkConfig": self.network_config,
            "RoleArn": self.role_arn,
            "StoppingCondition": 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
        self.subnets = subnets

    @property
    def response_object(self) -> dict[str, list[str]]:
        response_object = self.gen_response_object()
        return {
            k: v for k, v in response_object.items() if v is not None and v != [None]
        }


class Container(BaseObject):
    def __init__(self, **kwargs: Any):
        self.container_hostname = kwargs.get("container_hostname", "localhost")
        self.model_data_url = kwargs.get("data_url", "")
        self.model_package_name = kwargs.get("package_name", "pkg")
        self.image = kwargs.get("image", "")
        self.environment = kwargs.get("environment", {})

    @property
    def response_object(self) -> dict[str, Any]:  # type: ignore[misc]
        response_object = self.gen_response_object()
        return {
            k: v for k, v in response_object.items() if v is not None and v != [None]
        }


class FakeSagemakerNotebookInstance(CloudFormationModel):
    def __init__(
        self,
        account_id: str,
        region_name: str,
        notebook_instance_name: str,
        instance_type: str,
        role_arn: str,
        subnet_id: str | None,
        security_group_ids: list[str] | None,
        kms_key_id: str | None,
        tags: list[dict[str, str]] | None,
        lifecycle_config_name: str | None,
        direct_internet_access: str,
        volume_size_in_gb: int,
        accelerator_types: list[str] | None,
        default_code_repository: str | None,
        additional_code_repositories: list[str] | None,
        root_access: str | None,
    ):
        self.validate_volume_size_in_gb(volume_size_in_gb)
        self.validate_instance_type(instance_type)

        self.region_name = region_name
        self.notebook_instance_name = notebook_instance_name
        self.instance_type = instance_type
        self.role_arn = role_arn
        self.subnet_id = subnet_id
        self.security_group_ids = security_group_ids
        self.kms_key_id = kms_key_id
        self.tags = tags or []
        self.lifecycle_config_name = lifecycle_config_name
        self.direct_internet_access = direct_internet_access
        self.volume_size_in_gb = volume_size_in_gb
        self.accelerator_types = accelerator_types
        self.default_code_repository = default_code_repository
        self.additional_code_repositories = additional_code_repositories
        self.root_access = root_access
        self.status = "Pending"
        self.creation_time = self.last_modified_time = utcnow()
        self.arn = arn_formatter(
            "notebook-instance", notebook_instance_name, account_id, region_name
        )
        self.start()

    def validate_volume_size_in_gb(self, volume_size_in_gb: int) -> None:
        if not validators.is_integer_between(volume_size_in_gb, mn=5, optional=True):
            message = "Invalid range for parameter VolumeSizeInGB, value: {}, valid range: 5-inf"
            raise ValidationError(message=message)

    def validate_instance_type(self, instance_type: str) -> None:
        VALID_INSTANCE_TYPES = [
            "ml.p2.xlarge",
            "ml.m5.4xlarge",
            "ml.m4.16xlarge",
            "ml.t3.xlarge",
            "ml.p3.16xlarge",
            "ml.t2.xlarge",
            "ml.p2.16xlarge",
            "ml.c4.2xlarge",
            "ml.c5.2xlarge",
            "ml.c4.4xlarge",
            "ml.c5d.2xlarge",
            "ml.c5.4xlarge",
            "ml.c5d.4xlarge",
            "ml.c4.8xlarge",
            "ml.c5d.xlarge",
            "ml.c5.9xlarge",
            "ml.c5.xlarge",
            "ml.c5d.9xlarge",
            "ml.c4.xlarge",
            "ml.t2.2xlarge",
            "ml.c5d.18xlarge",
            "ml.t3.2xlarge",
            "ml.t3.medium",
            "ml.t2.medium",
            "ml.c5.18xlarge",
            "ml.p3.2xlarge",
            "ml.m5.xlarge",
            "ml.m4.10xlarge",
            "ml.t2.large",
            "ml.m5.12xlarge",
            "ml.m4.xlarge",
            "ml.t3.large",
            "ml.m5.24xlarge",
            "ml.m4.2xlarge",
            "ml.p2.8xlarge",
            "ml.m5.2xlarge",
            "ml.p3.8xlarge",
            "ml.m4.4xlarge",
        ]
        if not validators.is_one_of(instance_type, VALID_INSTANCE_TYPES):
            message = f"Value '{instance_type}' at 'instanceType' failed to satisfy constraint: Member must satisfy enum value set: {VALID_INSTANCE_TYPES}"
            raise ValidationError(message=message)

    @property
    def url(self) -> str:
        return (
            f"{self.notebook_instance_name}.notebook.{self.region_name}.sagemaker.aws"
        )

    def start(self) -> None:
        self.status = "InService"

    @property
    def is_deletable(self) -> bool:
        return self.status in ["Stopped", "Failed"]

    def stop(self) -> None:
        self.status = "Stopped"

    @property
    def physical_resource_id(self) -> str:
        return self.arn

    @classmethod
    def has_cfn_attr(cls, attr: str) -> bool:
        return attr in ["NotebookInstanceName"]

    def get_cfn_attribute(self, attribute_name: str) -> str:
        # https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-sagemaker-notebookinstance.html#aws-resource-sagemaker-notebookinstance-return-values
        from moto.cloudformation.exceptions import UnformattedGetAttTemplateException

        if attribute_name == "NotebookInstanceName":
            return self.notebook_instance_name
        raise UnformattedGetAttTemplateException()

    @staticmethod
    def cloudformation_name_type() -> str:
        return ""

    @staticmethod
    def cloudformation_type() -> str:
        # https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-sagemaker-notebookinstance.html
        return "AWS::SageMaker::NotebookInstance"

    @classmethod
    def create_from_cloudformation_json(  # type: ignore[misc]
        cls,
        resource_name: str,
        cloudformation_json: Any,
        account_id: str,
        region_name: str,
        **kwargs: Any,
    ) -> "FakeSagemakerNotebookInstance":
        # Get required properties from provided CloudFormation template
        properties = cloudformation_json["Properties"]
        instance_type = properties["InstanceType"]
        role_arn = properties["RoleArn"]

        notebook = sagemaker_backends[account_id][region_name].create_notebook_instance(
            notebook_instance_name=resource_name,
            instance_type=instance_type,
            role_arn=role_arn,
        )
        return notebook

    @classmethod
    def update_from_cloudformation_json(  # type: ignore[misc]
        cls,
        original_resource: Any,
        new_resource_name: str,
        cloudformation_json: Any,
        account_id: str,
        region_name: str,
    ) -> "FakeSagemakerNotebookInstance":
        # Operations keep same resource name so delete old and create new to mimic update
        cls.delete_from_cloudformation_json(
            original_resource.arn, cloudformation_json, account_id, region_name
        )
        new_resource = cls.create_from_cloudformation_json(
            original_resource.notebook_instance_name,
            cloudformation_json,
            account_id,
            region_name,
        )
        return new_resource

    @classmethod
    def delete_from_cloudformation_json(  # type: ignore[misc]
        cls,
        resource_name: str,
        cloudformation_json: Any,
        account_id: str,
        region_name: str,
    ) -> None:
        # Get actual name because resource_name actually provides the ARN
        # since the Physical Resource ID is the ARN despite SageMaker
        # using the name for most of its operations.
        notebook_instance_name = resource_name.split("/")[-1]

        backend = sagemaker_backends[account_id][region_name]
        backend.stop_notebook_instance(notebook_instance_name)
        backend.delete_notebook_instance(notebook_instance_name)

    def to_dict(self) -> dict[str, Any]:
        return {
            "NotebookInstanceArn": self.arn,
            "NotebookInstanceName": self.notebook_instance_name,
            "NotebookInstanceStatus": self.status,
            "Url": self.url,
            "InstanceType": self.instance_type,
            "SubnetId": self.subnet_id,
            "SecurityGroups": self.security_group_ids,
            "RoleArn": self.role_arn,
            "KmsKeyId": self.kms_key_id,
            # ToDo: NetworkInterfaceId
            "LastModifiedTime": self.last_modified_time,
            "CreationTime": self.creation_time,
            "NotebookInstanceLifecycleConfigName": self.lifecycle_config_name,
            "DirectInternetAccess": self.direct_internet_access,
            "VolumeSizeInGB": self.volume_size_in_gb,
            "AcceleratorTypes": self.accelerator_types,
            "DefaultCodeRepository": self.default_code_repository,
            "AdditionalCodeRepositories": self.additional_code_repositories,
            "RootAccess": self.root_access,
        }


class FakeSageMakerNotebookInstanceLifecycleConfig(BaseObject, CloudFormationModel):
    def __init__(
        self,
        account_id: str,
        region_name: str,
        notebook_instance_lifecycle_config_name: str,
        on_create: list[dict[str, str]],
        on_start: list[dict[str, str]],
    ):
        self.region_name = region_name
        self.notebook_instance_lifecycle_config_name = (
            notebook_instance_lifecycle_config_name
        )
        self.on_create = on_create
        self.on_start = on_start
        self.creation_time = self.last_modified_time = utcnow()
        self.arn = FakeSageMakerNotebookInstanceLifecycleConfig.arn_formatter(
            self.notebook_instance_lifecycle_config_name, account_id, region_name
        )

    @staticmethod
    def arn_formatter(name: str, account_id: str, region_name: str) -> str:
        return arn_formatter(
            "notebook-instance-lifecycle-config", name, account_id, region_name
        )

    @property
    def response_object(self) -> dict[str, Any]:  # type: ignore[misc]
        response_object = self.gen_response_object()
        response = {
            k: v for k, v in response_object.items() if v is not None and v != [None]
        }
        response["NotebookInstanceLifecycleConfigArn"] = response.pop("Arn")

        return response

    @property
    def physical_resource_id(self) -> str:
        return self.arn

    @classmethod
    def has_cfn_attr(cls, attr: str) -> bool:
        return attr in ["NotebookInstanceLifecycleConfigName"]

    def get_cfn_attribute(self, attribute_name: str) -> str:
        # https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-sagemaker-notebookinstancelifecycleconfig.html#aws-resource-sagemaker-notebookinstancelifecycleconfig-return-values
        from moto.cloudformation.exceptions import UnformattedGetAttTemplateException

        if attribute_name == "NotebookInstanceLifecycleConfigName":
            return self.notebook_instance_lifecycle_config_name
        raise UnformattedGetAttTemplateException()

    @staticmethod
    def cloudformation_name_type() -> str:
        return ""

    @staticmethod
    def cloudformation_type() -> str:
        # https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-sagemaker-notebookinstancelifecycleconfig.html
        return "AWS::SageMaker::NotebookInstanceLifecycleConfig"

    @classmethod
    def create_from_cloudformation_json(  # type: ignore[misc]
        cls,
        resource_name: str,
        cloudformation_json: Any,
        account_id: str,
        region_name: str,
        **kwargs: Any,
    ) -> "FakeSageMakerNotebookInstanceLifecycleConfig":
        properties = cloudformation_json["Properties"]

        config = sagemaker_backends[account_id][
            region_name
        ].create_notebook_instance_lifecycle_config(
            notebook_instance_lifecycle_config_name=resource_name,
            on_create=properties.get("OnCreate"),
            on_start=properties.get("OnStart"),
        )
        return config

    @classmethod
    def update_from_cloudformation_json(  # type: ignore[misc]
        cls,
        original_resource: Any,
        new_resource_name: str,
        cloudformation_json: Any,
        account_id: str,
        region_name: str,
    ) -> "FakeSageMakerNotebookInstanceLifecycleConfig":
        # Operations keep same resource name so delete old and create new to mimic update
        cls.delete_from_cloudformation_json(
            original_resource.arn,
            cloudformation_json,
            account_id,
            region_name,
        )
        new_resource = cls.create_from_cloudformation_json(
            original_resource.notebook_instance_lifecycle_config_name,
            cloudformation_json,
            account_id,
            region_name,
        )
        return new_resource

    @classmethod
    def delete_from_cloudformation_json(  # type: ignore[misc]
        cls,
        resource_name: str,
        cloudformation_json: Any,
        account_id: str,
        region_name: str,
    ) -> None:
        # Get actual name because resource_name actually provides the ARN
        # since the Physical Resource ID is the ARN despite SageMaker
        # using the name for most of its operations.
        config_name = resource_name.split("/")[-1]

        backend = sagemaker_backends[account_id][region_name]
        backend.delete_notebook_instance_lifecycle_config(config_name)


class SageMakerModelBackend(BaseBackend):
    def __init__(self, region_name: str, account_id: str):
        super().__init__(region_name, account_id)
        self._models: dict[str, Model] = {}
        self.notebook_instances: dict[str, FakeSagemakerNotebookInstance] = {}
        self.endpoint_configs: dict[str, FakeEndpointConfig] = {}
        self.endpoints: dict[str, FakeEndpoint] = {}
        self.experiments: dict[str, FakeExperiment] = {}
        self.pipelines: dict[str, FakePipeline] = {}
        self.pipeline_executions: dict[str, FakePipelineExecution] = {}
        self.processing_jobs: dict[str, FakeProcessingJob] = {}
        self.trials: dict[str, FakeTrial] = {}
        self.trial_components: dict[str, FakeTrialComponent] = {}
        self.training_jobs: dict[str, FakeTrainingJob] = {}
        self.transform_jobs: dict[str, FakeTransformJob] = {}
        self.notebook_instance_lifecycle_configurations: dict[
            str, FakeSageMakerNotebookInstanceLifecycleConfig
        ] = {}
        self.model_cards: defaultdict[str, list[FakeModelCard]] = defaultdict(list)
        self.model_package_groups: dict[str, ModelPackageGroup] = {}
        self.model_packages: dict[str, ModelPackage] = {}
        self.model_package_name_mapping: dict[str, str] = {}
        self.feature_groups: dict[str, FeatureGroup] = {}
        self.clusters: dict[str, Cluster] = {}
        self.data_quality_job_definitions: dict[str, FakeDataQualityJobDefinition] = {}
        self.model_bias_job_definitions: dict[str, FakeModelBiasJobDefinition] = {}
        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
        ] = {}
        self.hyper_parameter_tuning_jobs: dict[str, HyperParameterTuningJob] = {}
        self.model_quality_job_definitions: dict[str, ModelQualityJobDefinition] = {}

    @staticmethod
    def default_vpc_endpoint_service(
        service_region: str, zones: list[str]
    ) -> list[dict[str, str]]:
        """Default VPC endpoint services."""
        api_service = BaseBackend.default_vpc_endpoint_service_factory(
            service_region, zones, "api.sagemaker", special_service_name="sagemaker.api"
        )

        notebook_service_id = f"vpce-svc-{BaseBackend.vpce_random_number()}"
        studio_service_id = f"vpce-svc-{BaseBackend.vpce_random_number()}"

        notebook_service = {
            "AcceptanceRequired": False,
            "AvailabilityZones": zones,
            "BaseEndpointDnsNames": [
                f"{notebook_service_id}.{service_region}.vpce.amazonaws.com",
                f"notebook.{service_region}.vpce.sagemaker.aws",
            ],
            "ManagesVpcEndpoints": False,
            "Owner": "amazon",
            "PrivateDnsName": f"*.notebook.{service_region}.sagemaker.aws",
            "PrivateDnsNameVerificationState": "verified",
            "PrivateDnsNames": [
                {"PrivateDnsName": f"*.notebook.{service_region}.sagemaker.aws"}
            ],
            "ServiceId": notebook_service_id,
            "ServiceName": f"aws.sagemaker.{service_region}.notebook",
            "ServiceType": [{"ServiceType": "Interface"}],
            "Tags": [],
            "VpcEndpointPolicySupported": True,
        }
        studio_service = {
            "AcceptanceRequired": False,
            "AvailabilityZones": zones,
            "BaseEndpointDnsNames": [
                f"{studio_service_id}.{service_region}.vpce.amazonaws.com",
                f"studio.{service_region}.vpce.sagemaker.aws",
            ],
            "ManagesVpcEndpoints": False,
            "Owner": "amazon",
            "PrivateDnsName": f"*.studio.{service_region}.sagemaker.aws",
            "PrivateDnsNameVerificationState": "verified",
            "PrivateDnsNames": [
                {"PrivateDnsName": f"*.studio.{service_region}.sagemaker.aws"}
            ],
            "ServiceId": studio_service_id,
            "ServiceName": f"aws.sagemaker.{service_region}.studio",
            "ServiceType": [{"ServiceType": "Interface"}],
            "Tags": [],
            "VpcEndpointPolicySupported": True,
        }
        return api_service + [notebook_service, studio_service]

    def create_model(
        self,
        model_name: str,
        execution_role_arn: str,
        primary_container: dict[str, Any] | None,
        vpc_config: dict[str, Any] | None,
        containers: list[dict[str, Any]] | None,
        tags: list[dict[str, str]] | None,
    ) -> Model:
        model_obj = Model(
            account_id=self.account_id,
            region_name=self.region_name,
            model_name=model_name,
            execution_role_arn=execution_role_arn,
            primary_container=primary_container or {},
            vpc_config=vpc_config or {},
            containers=containers or [],
            tags=tags or [],
        )

        self._models[model_name] = model_obj
        return model_obj

    def describe_model(self, model_name: str) -> Model:
        model = self._models.get(model_name)
        if model:
            return model
        arn = arn_formatter("model", model_name, self.account_id, self.region_name)
        raise ValidationError(message=f"Could not find model '{arn}'.")

    def list_models(self) -> Iterable[Model]:
        return self._models.values()

    def delete_model(self, model_name: str) -> None:
        for model in self._models.values():
            if model.model_name == model_name:
                self._models.pop(model.model_name)
                break
        else:
            raise MissingModel(model=model_name)

    def create_experiment(self, experiment_name: str) -> dict[str, str]:
        experiment = FakeExperiment(
            account_id=self.account_id,
            region_name=self.region_name,
            experiment_name=experiment_name,
            tags=[],
        )
        self.experiments[experiment_name] = experiment
        return experiment.response_create

    def describe_experiment(self, experiment_name: str) -> dict[str, Any]:
        experiment_data = self.experiments[experiment_name]
        return {
            "ExperimentName": experiment_data.experiment_name,
            "ExperimentArn": experiment_data.arn,
            "CreationTime": experiment_data.creation_time,
            "LastModifiedTime": experiment_data.last_modified_time,
        }

    def _get_resource_from_arn(self, arn: str) -> Any:
        resources = {
            "model": self._models,
            "notebook-instance": self.notebook_instances,
            "endpoint": self.endpoints,
            "endpoint-config": self.endpoint_configs,
            "training-job": self.training_jobs,
            "transform-job": self.transform_jobs,
            "experiment": self.experiments,
            "experiment-trial": self.trials,
            "experiment-trial-component": self.trial_components,
            "processing-job": self.processing_jobs,
            "pipeline": self.pipelines,
            "model-package-group": self.model_package_groups,
            "cluster": self.clusters,
            "data-quality-job-definition": self.data_quality_job_definitions,
            "model-bias-job-definition": self.model_bias_job_definitions,
            "automl-job": self.auto_ml_jobs,
            "compilation-job": self.compilation_jobs,
            "domain": self.domains,
            "model-explainability-job-definition": self.model_explainability_job_definitions,
            "hyper-parameter-tuning-job": self.hyper_parameter_tuning_jobs,
            "model-quality-job-definition": self.model_quality_job_definitions,
            "model-card": self.model_cards,
        }
        target_resource, target_name = arn.split(":")[-1].split("/")
        try:
            resource = resources.get(target_resource).get(target_name)  # type: ignore
        except KeyError:
            message = f"Could not find {target_resource} with name {target_name}"
            raise ValidationError(message=message)
        if isinstance(resource, list):
            return resource[0]
        return resource

    def add_tags(self, arn: str, tags: list[dict[str, str]]) -> list[dict[str, str]]:
        resource = self._get_resource_from_arn(arn)
        resource.tags.extend(tags)
        return resource.tags

    @paginate(pagination_model=PAGINATION_MODEL)
    def list_tags(self, arn: str) -> list[dict[str, str]]:
        resource = self._get_resource_from_arn(arn)
        return resource.tags

    def delete_tags(self, arn: str, tag_keys: list[str]) -> None:
        resource = self._get_resource_from_arn(arn)
        resource.tags = [tag for tag in resource.tags if tag["Key"] not in tag_keys]

    @paginate(pagination_model=PAGINATION_MODEL)
    def list_experiments(self) -> list["FakeExperiment"]:
        return list(self.experiments.values())

    def search(self, resource: Any = None, search_expression: Any = None) -> Any:
        """
        Only a few SearchExpressions are implemented. Please open a bug report if you find any issues.
        """
        next_index = None

        valid_resources = {
            "Pipeline": self.pipelines.values(),
            "ModelPackageGroup": self.model_package_groups.values(),
            "TrainingJob": self.training_jobs.values(),
            "ExperimentTrialComponent": self.trial_components.values(),
            "FeatureGroup": self.feature_groups.values(),
            "Endpoint": self.endpoints.values(),
            "PipelineExecution": self.pipeline_executions.values(),
            "Project": [],
            "ExperimentTrial": self.trials.values(),
            "Image": [],
            "ImageVersion": [],
            "ModelPackage": self.model_packages.values(),
            "Experiment": self.experiments.values(),
        }

        if resource not in valid_resources:
            raise AWSValidationException(
                f"An error occurred (ValidationException) when calling the Search operation: 1 validation error detected: Value '{resource}' at 'resource' failed to satisfy constraint: Member must satisfy enum value set: {valid_resources}"
            )

        def compare_value(actual: Any, expected: Any, operator: str) -> bool:
            # Defeault:  operator == "Equals"
            if operator == "Contains":
                return expected in actual
            if operator == "NotEquals":
                return expected != actual
            return actual == expected

        def evaluate_search_expression(item: Any) -> bool:
            filters = None
            if search_expression is not None:
                filters = search_expression.get("Filters")

            if filters is not None:
                for f in filters:
                    prop_key = camelcase_to_underscores(f["Name"])
                    if f["Name"].startswith("Tags."):
                        key = f["Name"][5:]
                        value = f["Value"]

                        if f["Operator"] == "Equals":
                            if not [
                                e
                                for e in item.tags
                                if e["Key"] == key and e["Value"] == value
                            ]:
                                return False
                        return True

                    elif f["Name"] == "TrialName":
                        raise AWSValidationException(
                            f"An error occurred (ValidationException) when calling the Search operation: Unknown property name: {f['Name']}"
                        )

                    elif f["Name"] == "Parents.TrialName":
                        trial_name = f["Value"]
                        if item.trial_name != trial_name:
                            return False

                    elif hasattr(item, prop_key):
                        if not compare_value(
                            getattr(item, prop_key), f["Value"], f["Operator"]
                        ):
                            return False
                    else:
                        raise ValidationError(
                            message=f"Unknown property name: {f['Name']}"
                        )

            return True

        result: dict[str, Any] = {
            "Results": [],
            "NextToken": str(next_index) if next_index is not None else None,
        }
        # ResourceName, ResultName, Resources
        result_names = {
            "ExperimentTrial": "Trial",
            "ExperimentTrialComponent": "TrialComponent",
        }
        resources_found = [
            x for x in valid_resources[resource] if evaluate_search_expression(x)
        ]
        result_name = result_names.get(resource, resource)
        for found in resources_found:
            result["Results"].append({result_name: found.gen_response_object()})

        return result

    def delete_experiment(self, experiment_name: str) -> None:
        try:
            del self.experiments[experiment_name]
        except KeyError:
            arn = FakeTrial.arn_formatter(
                experiment_name, self.account_id, self.region_name
            )
            raise ValidationError(
                message=f"Could not find experiment configuration '{arn}'."
            )

    def create_trial(self, trial_name: str, experiment_name: str) -> dict[str, str]:
        trial = FakeTrial(
            account_id=self.account_id,
            region_name=self.region_name,
            trial_name=trial_name,
            experiment_name=experiment_name,
            tags=[],
            trial_components=[],
        )
        self.trials[trial_name] = trial
        return trial.response_create

    def describe_trial(self, trial_name: str) -> dict[str, Any]:
        try:
            return self.trials[trial_name].response_object
        except KeyError:
            arn = FakeTrial.arn_formatter(trial_name, self.account_id, self.region_name)
            raise ValidationError(message=f"Could not find trial '{arn}'.")

    def delete_trial(self, trial_name: str) -> None:
        try:
            del self.trials[trial_name]
        except KeyError:
            arn = FakeTrial.arn_formatter(trial_name, self.account_id, self.region_name)
            raise ValidationError(
                message=f"Could not find trial configuration '{arn}'."
            )

    @paginate(pagination_model=PAGINATION_MODEL)
    def list_trials(
        self,
        experiment_name: str | None = None,
        trial_component_name: str | None = None,
    ) -> list["FakeTrial"]:
        trials_fetched = list(self.trials.values())

        def evaluate_filter_expression(trial_data: FakeTrial) -> bool:
            if experiment_name is not None:
                if trial_data.experiment_name != experiment_name:
                    return False

            if trial_component_name is not None:
                if trial_component_name not in trial_data.trial_components:
                    return False

            return True

        return [
            trial_data
            for trial_data in trials_fetched
            if evaluate_filter_expression(trial_data)
        ]

    def create_trial_component(
        self,
        trial_component_name: str,
        trial_name: str,
        status: dict[str, str],
        start_time: datetime | None,
        end_time: datetime | None,
        display_name: str | None,
        parameters: dict[str, dict[str, str | float]] | None,
        input_artifacts: dict[str, dict[str, str]] | None,
        output_artifacts: dict[str, dict[str, str]] | None,
        metadata_properties: dict[str, str] | None,
    ) -> dict[str, Any]:
        trial_component = FakeTrialComponent(
            account_id=self.account_id,
            region_name=self.region_name,
            display_name=display_name,
            start_time=start_time,
            end_time=end_time,
            parameters=parameters,
            input_artifacts=input_artifacts,
            output_artifacts=output_artifacts,
            metadata_properties=metadata_properties,
            trial_component_name=trial_component_name,
            trial_name=trial_name,
            status=status,
            tags=[],
        )
        self.trial_components[trial_component_name] = trial_component
        return trial_component.response_create

    def delete_trial_component(self, trial_component_name: str) -> None:
        try:
            del self.trial_components[trial_component_name]
        except KeyError:
            arn = FakeTrial.arn_formatter(
                trial_component_name, self.account_id, self.region_name
            )
            raise ValidationError(
                message=f"Could not find trial-component configuration '{arn}'."
            )

    def describe_trial_component(self, trial_component_name: str) -> dict[str, Any]:
        try:
            return self.trial_components[trial_component_name].response_object
        except KeyError:
            arn = FakeTrialComponent.arn_formatter(
                trial_component_name, self.account_id, self.region_name
            )
            raise ValidationError(message=f"Could not find trial component '{arn}'.")

    def _update_trial_component_details(
        self, trial_component_name: str, details_json: str
    ) -> None:
        self.trial_components[trial_component_name].update(details_json)

    @paginate(pagination_model=PAGINATION_MODEL)
    def list_trial_components(
        self, trial_name: str | None = None
    ) -> list["FakeTrialComponent"]:
        trial_components_fetched = list(self.trial_components.values())

        return [
            trial_component_data
            for trial_component_data in trial_components_fetched
            if trial_name is None or trial_component_data.trial_name == trial_name
        ]

    def associate_trial_component(
        self, trial_name: str, trial_component_name: str
    ) -> dict[str, str]:
        if trial_name in self.trials.keys():
            self.trials[trial_name].trial_components.extend([trial_component_name])
        else:
            raise ResourceNotFound(
                message=f"Trial 'arn:{get_partition(self.region_name)}:sagemaker:{self.region_name}:{self.account_id}:experiment-trial/{trial_name}' does not exist."
            )

        if trial_component_name in self.trial_components.keys():
            self.trial_components[trial_component_name].trial_name = trial_name

        return {
            "TrialComponentArn": self.trial_components[trial_component_name].arn,
            "TrialArn": self.trials[trial_name].arn,
        }

    def disassociate_trial_component(
        self, trial_name: str, trial_component_name: str
    ) -> dict[str, str]:
        if trial_component_name in self.trial_components.keys():
            self.trial_components[trial_component_name].trial_name = None

        if trial_name in self.trials.keys():
            self.trials[trial_name].trial_components = list(
                filter(
                    lambda x: x != trial_component_name,
                    self.trials[trial_name].trial_components,
                )
            )

        return {
            "TrialComponentArn": f"arn:{get_partition(self.region_name)}:sagemaker:{self.region_name}:{self.account_id}:experiment-trial-component/{trial_component_name}",
            "TrialArn": f"arn:{get_partition(self.region_name)}:sagemaker:{self.region_name}:{self.account_id}:experiment-trial/{trial_name}",
        }

    def update_trial_component(
        self,
        trial_component_name: str,
        status: dict[str, str] | None,
        display_name: str | None,
        start_time: datetime | None,
        end_time: datetime | None,
        parameters: dict[str, dict[str, str | float]] | None,
        parameters_to_remove: list[str] | None,
        input_artifacts: dict[str, dict[str, str]] | None,
        input_artifacts_to_remove: list[str] | None,
        output_artifacts: dict[str, dict[str, str]] | None,
        output_artifacts_to_remove: list[str] | None,
    ) -> dict[str, str]:
        try:
            trial_component = self.trial_components[trial_component_name]
        except KeyError:
            arn = FakeTrialComponent.arn_formatter(
                trial_component_name, self.account_id, self.region_name
            )
            raise ValidationError(message=f"Could not find trial component '{arn}'")

        if status:
            trial_component.status = status
        if display_name:
            trial_component.display_name = display_name
        if start_time:
            trial_component.start_time = start_time
        if end_time:
            trial_component.end_time = end_time
        if parameters:
            trial_component.parameters = parameters
        if input_artifacts:
            trial_component.input_artifacts = input_artifacts
        if output_artifacts:
            trial_component.output_artifacts = output_artifacts

        trial_component.last_modified_time = utcnow()

        for parameter_to_remove in parameters_to_remove or []:
            trial_component.parameters.pop(parameter_to_remove)

        for input_artifact_to_remove in input_artifacts_to_remove or []:
            trial_component.input_artifacts.pop(input_artifact_to_remove)

        for output_artifact_to_remove in output_artifacts_to_remove or []:
            trial_component.output_artifacts.pop(output_artifact_to_remove)

        return {
            "TrialComponentArn": FakeTrialComponent.arn_formatter(
                trial_component_name, self.account_id, self.region_name
            )
        }

    def create_notebook_instance(
        self,
        notebook_instance_name: str,
        instance_type: str,
        role_arn: str,
        subnet_id: str | None = None,
        security_group_ids: list[str] | None = None,
        kms_key_id: str | None = None,
        tags: list[dict[str, str]] | None = None,
        lifecycle_config_name: str | None = None,
        direct_internet_access: str = "Enabled",
        volume_size_in_gb: int = 5,
        accelerator_types: list[str] | None = None,
        default_code_repository: str | None = None,
        additional_code_repositories: list[str] | None = None,
        root_access: str | None = None,
    ) -> FakeSagemakerNotebookInstance:
        self._validate_unique_notebook_instance_name(notebook_instance_name)

        notebook_instance = FakeSagemakerNotebookInstance(
            account_id=self.account_id,
            region_name=self.region_name,
            notebook_instance_name=notebook_instance_name,
            instance_type=instance_type,
            role_arn=role_arn,
            subnet_id=subnet_id,
            security_group_ids=security_group_ids,
            kms_key_id=kms_key_id,
            tags=tags,
            lifecycle_config_name=lifecycle_config_name,
            direct_internet_access=(
                direct_internet_access
                if direct_internet_access is not None
                else "Enabled"
            ),
            volume_size_in_gb=volume_size_in_gb if volume_size_in_gb is not None else 5,
            accelerator_types=accelerator_types,
            default_code_repository=default_code_repository,
            additional_code_repositories=additional_code_repositories,
            root_access=root_access,
        )
        self.notebook_instances[notebook_instance_name] = notebook_instance
        return notebook_instance

    def _validate_unique_notebook_instance_name(
        self, notebook_instance_name: str
    ) -> None:
        if notebook_instance_name in self.notebook_instances:
            duplicate_arn = self.notebook_instances[notebook_instance_name].arn
            message = f"Cannot create a duplicate Notebook Instance ({duplicate_arn})"
            raise ValidationError(message=message)

    def get_notebook_instance(
        self, notebook_instance_name: str
    ) -> FakeSagemakerNotebookInstance:
        try:
            return self.notebook_instances[notebook_instance_name]
        except KeyError:
            raise ValidationError(message="RecordNotFound")

    def start_notebook_instance(self, notebook_instance_name: str) -> None:
        notebook_instance = self.get_notebook_instance(notebook_instance_name)
        notebook_instance.start()

    def stop_notebook_instance(self, notebook_instance_name: str) -> None:
        notebook_instance = self.get_notebook_instance(notebook_instance_name)
        notebook_instance.stop()

    def delete_notebook_instance(self, notebook_instance_name: str) -> None:
        notebook_instance = self.get_notebook_instance(notebook_instance_name)
        if not notebook_instance.is_deletable:
            message = f"Status ({notebook_instance.status}) not in ([Stopped, Failed]). Unable to transition to (Deleting) for Notebook Instance ({notebook_instance.arn})"
            raise ValidationError(message=message)
        del self.notebook_instances[notebook_instance_name]

    @paginate(pagination_model=PAGINATION_MODEL)
    def list_notebook_instances(
        self,
        sort_by: str,
        sort_order: str,
        name_contains: str | None,
        status: str | None,
    ) -> list[FakeSagemakerNotebookInstance]:
        """
        The following parameters are not yet implemented:
        CreationTimeBefore, CreationTimeAfter, LastModifiedTimeBefore, LastModifiedTimeAfter, NotebookInstanceLifecycleConfigNameContains, DefaultCodeRepositoryContains, AdditionalCodeRepositoryEquals
        """
        instances = list(self.notebook_instances.values())
        if name_contains:
            instances = [
                i for i in instances if name_contains in i.notebook_instance_name
            ]
        if status:
            instances = [i for i in instances if i.status == status]
        reverse = sort_order == "Descending"
        if sort_by == "Name":
            instances = sorted(
                instances, key=lambda x: x.notebook_instance_name, reverse=reverse
            )
        if sort_by == "CreationTime":
            instances = sorted(
                instances, key=lambda x: x.creation_time, reverse=reverse
            )
        if sort_by == "Status":
            instances = sorted(instances, key=lambda x: x.status, reverse=reverse)
        return instances

    def create_notebook_instance_lifecycle_config(
        self,
        notebook_instance_lifecycle_config_name: str,
        on_create: list[dict[str, str]],
        on_start: list[dict[str, str]],
    ) -> FakeSageMakerNotebookInstanceLifecycleConfig:
        if (
            notebook_instance_lifecycle_config_name
            in self.notebook_instance_lifecycle_configurations
        ):
            arn = FakeSageMakerNotebookInstanceLifecycleConfig.arn_formatter(
                notebook_instance_lifecycle_config_name,
                self.account_id,
                self.region_name,
            )
            message = f"Unable to create Notebook Instance Lifecycle Config {arn}. (Details: Notebook Instance Lifecycle Config already exists.)"
            raise ValidationError(message=message)
        lifecycle_config = FakeSageMakerNotebookInstanceLifecycleConfig(
            account_id=self.account_id,
            region_name=self.region_name,
            notebook_instance_lifecycle_config_name=notebook_instance_lifecycle_config_name,
            on_create=on_create,
            on_start=on_start,
        )
        self.notebook_instance_lifecycle_configurations[
            notebook_instance_lifecycle_config_name
        ] = lifecycle_config
        return lifecycle_config

    def describe_notebook_instance_lifecycle_config(
        self, notebook_instance_lifecycle_config_name: str
    ) -> dict[str, Any]:
        try:
            return self.notebook_instance_lifecycle_configurations[
                notebook_instance_lifecycle_config_name
            ].response_object
        except KeyError:
            arn = FakeSageMakerNotebookInstanceLifecycleConfig.arn_formatter(
                notebook_instance_lifecycle_config_name,
                self.account_id,
                self.region_name,
            )
            message = f"Unable to describe Notebook Instance Lifecycle Config '{arn}'. (Details: Notebook Instance Lifecycle Config does not exist.)"
            raise ValidationError(message=message)

    def delete_notebook_instance_lifecycle_config(
        self, notebook_instance_lifecycle_config_name: str
    ) -> None:
        try:
            del self.notebook_instance_lifecycle_configurations[
                notebook_instance_lifecycle_config_name
            ]
        except KeyError:
            arn = FakeSageMakerNotebookInstanceLifecycleConfig.arn_formatter(
                notebook_instance_lifecycle_config_name,
                self.account_id,
                self.region_name,
            )
            message = f"Unable to delete Notebook Instance Lifecycle Config '{arn}'. (Details: Notebook Instance Lifecycle Config does not exist.)"
            raise ValidationError(message=message)

    def create_endpoint_config(
        self,
        endpoint_config_name: str,
        production_variants: list[dict[str, Any]],
        data_capture_config: dict[str, Any],
        async_inference_config: dict[str, Any],
        tags: list[dict[str, str]],
        kms_key_id: str,
    ) -> FakeEndpointConfig:
        endpoint_config = FakeEndpointConfig(
            account_id=self.account_id,
            region_name=self.region_name,
            endpoint_config_name=endpoint_config_name,
            production_variants=production_variants,
            async_inference_config=async_inference_config,
            data_capture_config=data_capture_config,
            tags=tags,
            kms_key_id=kms_key_id,
        )
        self.validate_production_variants(production_variants)

        self.endpoint_configs[endpoint_config_name] = endpoint_config
        return endpoint_config

    def validate_production_variants(
        self, production_variants: list[dict[str, Any]]
    ) -> None:
        for production_variant in production_variants:
            if production_variant["ModelName"] not in self._models:
                arn = arn_formatter(
                    "model",
                    production_variant["ModelName"],
                    self.account_id,
                    self.region_name,
                )
                raise ValidationError(message=f"Could not find model '{arn}'.")

    def describe_endpoint_config(self, endpoint_config_name: str) -> dict[str, Any]:
        try:
            return self.endpoint_configs[endpoint_config_name].response_object
        except KeyError:
            arn = FakeEndpointConfig.arn_formatter(
                endpoint_config_name, self.account_id, self.region_name
            )
            raise ValidationError(
                message=f"Could not find endpoint configuration '{arn}'."
            )

    def delete_endpoint_config(self, endpoint_config_name: str) -> None:
        try:
            del self.endpoint_configs[endpoint_config_name]
        except KeyError:
            arn = FakeEndpointConfig.arn_formatter(
                endpoint_config_name, self.account_id, self.region_name
            )
            raise ValidationError(
                message=f"Could not find endpoint configuration '{arn}'."
            )

    def create_endpoint(
        self, endpoint_name: str, endpoint_config_name: str, tags: list[dict[str, str]]
    ) -> FakeEndpoint:
        try:
            endpoint_config = self.describe_endpoint_config(endpoint_config_name)
        except KeyError:
            arn = FakeEndpointConfig.arn_formatter(
                endpoint_config_name, self.account_id, self.region_name
            )
            raise ValidationError(message=f"Could not find endpoint_config '{arn}'.")

        endpoint = FakeEndpoint(
            account_id=self.account_id,
            region_name=self.region_name,
            endpoint_name=endpoint_name,
            endpoint_config_name=endpoint_config_name,
            production_variants=endpoint_config["ProductionVariants"],
            data_capture_config=endpoint_config["DataCaptureConfig"],
            tags=tags,
        )

        self.endpoints[endpoint_name] = endpoint
        return endpoint

    def describe_endpoint(self, endpoint_name: str) -> dict[str, Any]:
        try:
            return self.endpoints[endpoint_name].response_object
        except KeyError:
            arn = FakeEndpoint.arn_formatter(
                endpoint_name, self.account_id, self.region_name
            )
            raise ValidationError(message=f"Could not find endpoint '{arn}'.")

    def delete_endpoint(self, endpoint_name: str) -> None:
        try:
            del self.endpoints[endpoint_name]
        except KeyError:
            arn = FakeEndpoint.arn_formatter(
                endpoint_name, self.account_id, self.region_name
            )
            raise ValidationError(message=f"Could not find endpoint '{arn}'.")

    def create_processing_job(
        self,
        app_specification: dict[str, Any],
        experiment_config: dict[str, str],
        network_config: dict[str, Any],
        processing_inputs: list[dict[str, Any]],
        processing_job_name: str,
        processing_output_config: dict[str, Any],
        role_arn: str,
        tags: list[dict[str, str]],
        stopping_condition: dict[str, int],
    ) -> FakeProcessingJob:
        processing_job = FakeProcessingJob(
            app_specification=app_specification,
            experiment_config=experiment_config,
            network_config=network_config,
            processing_inputs=processing_inputs,
            processing_job_name=processing_job_name,
            processing_output_config=processing_output_config,
            account_id=self.account_id,
            region_name=self.region_name,
            role_arn=role_arn,
            stopping_condition=stopping_condition,
            tags=tags,
        )
        self.processing_jobs[processing_job_name] = processing_job
        return processing_job

    def describe_processing_job(self, processing_job_name: str) -> dict[str, Any]:
        try:
            return self.processing_jobs[processing_job_name].response_object
        except KeyError:
            arn = FakeProcessingJob.arn_formatter(
                processing_job_name, self.account_id, self.region_name
            )
            raise ValidationError(message=f"Could not find processing job '{arn}'.")

    def create_pipeline(
        self,
        pipeline_name: str,
        pipeline_display_name: str,
        pipeline_definition: str,
        pipeline_definition_s3_location: dict[str, Any],
        pipeline_description: str,
        role_arn: str,
        tags: list[dict[str, str]],
        parallelism_configuration: dict[str, int],
    ) -> FakePipeline:
        if not any([pipeline_definition, pipeline_definition_s3_location]):
            raise ValidationError(
                "An error occurred (ValidationException) when calling the CreatePipeline operation: Either "
                "Pipeline Definition or Pipeline Definition S3 location should be provided"
            )
        if all([pipeline_definition, pipeline_definition_s3_location]):
            raise ValidationError(
                "An error occurred (ValidationException) when calling the CreatePipeline operation: "
                "Both Pipeline Definition and Pipeline Definition S3 Location shouldn't be present"
            )

        if pipeline_name in self.pipelines:
            raise ValidationError(
                f"An error occurred (ValidationException) when calling the CreatePipeline operation: Pipeline names "
                f"must be unique within an AWS account and region. Pipeline with name ({pipeline_name}) already exists."
            )

        if pipeline_definition_s3_location:
            pipeline_definition = load_pipeline_definition_from_s3(  # type: ignore
                pipeline_definition_s3_location,
                account_id=self.account_id,
                partition=self.partition,
            )

        pipeline = FakePipeline(
            pipeline_name,
            pipeline_display_name,
            pipeline_definition,
            pipeline_description,
            role_arn,
            tags,
            self.account_id,
            self.region_name,
            parallelism_configuration,
        )

        self.pipelines[pipeline_name] = pipeline
        return pipeline

    def delete_pipeline(self, pipeline_name: str) -> str:
        pipeline = get_pipeline_from_name(self.pipelines, pipeline_name)
        del self.pipelines[pipeline.pipeline_name]
        return pipeline.arn

    def update_pipeline(self, pipeline_name: str, **kwargs: Any) -> str:
        pipeline = get_pipeline_from_name(self.pipelines, pipeline_name)
        if all(
            [
                kwargs.get("pipeline_definition"),
                kwargs.get("pipeline_definition_s3_location"),
            ]
        ):
            raise ValidationError(
                "An error occurred (ValidationException) when calling the UpdatePipeline operation: "
                "Both Pipeline Definition and Pipeline Definition S3 Location shouldn't be present"
            )

        for attr_key, attr_value in kwargs.items():
            if attr_value:
                if attr_key == "pipeline_definition_s3_location":
                    self.pipelines[
                        pipeline_name
                    ].pipeline_definition = load_pipeline_definition_from_s3(  # type: ignore
                        attr_value,
                        self.account_id,
                        partition=self.partition,
                    )
                    continue
                setattr(self.pipelines[pipeline_name], attr_key, attr_value)

        return pipeline.arn

    def start_pipeline_execution(
        self,
        pipeline_name: str,
        pipeline_execution_display_name: str,
        pipeline_parameters: list[dict[str, Any]],
        pipeline_execution_description: str,
        parallelism_configuration: dict[str, int],
        client_request_token: str,
    ) -> dict[str, str]:
        pipeline = get_pipeline_from_name(self.pipelines, pipeline_name)
        execution_id = "".join(
            random.choices(string.ascii_lowercase + string.digits, k=12)
        )
        pipeline_execution_arn = arn_formatter(
            _type="pipeline",
            _id=f"{pipeline.pipeline_name}/execution/{execution_id}",
            account_id=self.account_id,
            region_name=self.region_name,
        )

        fake_pipeline_execution = FakePipelineExecution(
            pipeline_execution_arn=pipeline_execution_arn,
            pipeline_execution_display_name=pipeline_execution_display_name,
            pipeline_parameters=pipeline_parameters,
            pipeline_execution_description=pipeline_execution_description,
            pipeline_definition=pipeline.pipeline_definition,
            parallelism_configuration=parallelism_configuration
            or pipeline.parallelism_configuration,
            client_request_token=client_request_token,
        )

        self.pipelines[pipeline_name].pipeline_executions[pipeline_execution_arn] = (
            fake_pipeline_execution
        )
        self.pipelines[
            pipeline_name
        ].last_execution_time = fake_pipeline_execution.start_time

        return {"PipelineExecutionArn": pipeline_execution_arn}

    def list_pipeline_executions(self, pipeline_name: str) -> dict[str, Any]:
        pipeline = get_pipeline_from_name(self.pipelines, pipeline_name)
        return {
            "PipelineExecutionSummaries": [
                {
                    "PipelineExecutionArn": arn,
                    "StartTime": pipeline_execution.start_time,
                    "PipelineExecutionStatus": pipeline_execution.pipeline_execution_status,
                    "PipelineExecutionDescription": pipeline_execution.pipeline_execution_description,
                    "PipelineExecutionDisplayName": pipeline_execution.pipeline_execution_display_name,
                    "PipelineExecutionFailureReason": str(
                        pipeline_execution.pipeline_execution_failure_reason
                    ),
                }
                for arn, pipeline_execution in pipeline.pipeline_executions.items()
            ]
        }

    def describe_pipeline_definition_for_execution(
        self, pipeline_execution_arn: str
    ) -> dict[str, Any]:
        pipeline_execution = get_pipeline_execution_from_arn(
            self.pipelines, pipeline_execution_arn
        )
        return {
            "PipelineDefinition": str(
                pipeline_execution.pipeline_definition_for_execution
            ),
            "CreationTime": pipeline_execution.creation_time,
        }

    def list_pipeline_parameters_for_execution(
        self, pipeline_execution_arn: str
    ) -> dict[str, Any]:
        pipeline_execution = get_pipeline_execution_from_arn(
            self.pipelines, pipeline_execution_arn
        )
        return {
            "PipelineParameters": pipeline_execution.pipeline_parameters,
        }

    def describe_pipeline_execution(
        self, pipeline_execution_arn: str
    ) -> dict[str, Any]:
        pipeline_execution = get_pipeline_execution_from_arn(
            self.pipelines, pipeline_execution_arn
        )
        pipeline_name = get_pipeline_name_from_execution_arn(pipeline_execution_arn)
        pipeline = get_pipeline_from_name(self.pipelines, pipeline_name)

        return {
            "PipelineArn": pipeline.arn,
            "PipelineExecutionArn": pipeline_execution.arn,
            "PipelineExecutionDisplayName": pipeline_execution.pipeline_execution_display_name,
            "PipelineExecutionStatus": pipeline_execution.pipeline_execution_status,
            "PipelineExecutionDescription": pipeline_execution.pipeline_execution_description,
            "PipelineExperimentConfig": {},
            "FailureReason": "",
            "CreationTime": pipeline_execution.creation_time,
            "LastModifiedTime": pipeline_execution.last_modified_time,
            "CreatedBy": pipeline_execution.created_by,
            "LastModifiedBy": pipeline_execution.last_modified_by,
            "ParallelismConfiguration": pipeline_execution.parallelism_configuration,
        }

    def describe_pipeline(self, pipeline_name: str) -> dict[str, Any]:
        pipeline = get_pipeline_from_name(self.pipelines, pipeline_name)
        return {
            "PipelineArn": pipeline.arn,
            "PipelineName": pipeline.pipeline_name,
            "PipelineDisplayName": pipeline.pipeline_display_name,
            "PipelineDescription": pipeline.pipeline_description,
            "PipelineDefinition": pipeline.pipeline_definition,
            "RoleArn": pipeline.role_arn,
            "PipelineStatus": pipeline.pipeline_status,
            "CreationTime": pipeline.creation_time,
            "LastModifiedTime": pipeline.last_modified_time,
            "LastRunTime": pipeline.last_execution_time,
            "CreatedBy": pipeline.created_by,
            "LastModifiedBy": pipeline.last_modified_by,
            "ParallelismConfiguration": pipeline.parallelism_configuration,
        }

    def list_pipelines(
        self,
        pipeline_name_prefix: str,
        created_after: datetime | None,
        created_before: datetime | None,
        next_token: str,
        max_results: int,
        sort_by: str,
        sort_order: str,
    ) -> dict[str, Any]:
        if next_token:
            try:
                starting_index = int(next_token)
                if starting_index > len(self.pipelines):
                    raise ValueError  # invalid next_token
            except ValueError:
                raise AWSValidationException('Invalid pagination token because "{0}".')
        else:
            starting_index = 0

        if max_results:
            end_index = max_results + starting_index
            pipelines_fetched: Iterable[FakePipeline] = list(self.pipelines.values())[
                starting_index:end_index
            ]
            if end_index >= len(self.pipelines):
                next_index = None
            else:
                next_index = end_index
        else:
            pipelines_fetched = list(self.pipelines.values())
            next_index = None

        if pipeline_name_prefix is not None:
            pipelines_fetched = filter(
                lambda x: pipeline_name_prefix in x.pipeline_name,
                pipelines_fetched,
            )

        if created_after is not None:
            pipelines_fetched = filter(
                lambda x: x.creation_time > created_after,
                pipelines_fetched,
            )

        if created_before is not None:
            pipelines_fetched = filter(
                lambda x: x.creation_time < created_before,
                pipelines_fetched,
            )

        sort_key = "pipeline_name" if sort_by == "Name" else "creation_time"
        pipelines_fetched = sorted(
            pipelines_fetched,
            key=lambda pipeline_fetched: getattr(pipeline_fetched, sort_key),
            reverse=sort_order != "Ascending",
        )

        pipeline_summaries = [
            {
                "PipelineArn": pipeline_data.arn,
                "PipelineName": pipeline_data.pipeline_name,
                "PipelineDisplayName": pipeline_data.pipeline_display_name,
                "PipelineDescription": pipeline_data.pipeline_description,
                "RoleArn": pipeline_data.role_arn,
                "CreationTime": pipeline_data.creation_time,
                "LastModifiedTime": pipeline_data.last_modified_time,
                "LastExecutionTime": pipeline_data.last_execution_time,
            }
            for pipeline_data in pipelines_fetched
        ]

        return {
            "PipelineSummaries": pipeline_summaries,
            "NextToken": str(next_index) if next_index is not None else None,
        }

    def list_processing_jobs(
        self,
        next_token: str,
        max_results: int,
        creation_time_after: datetime | None,
        creation_time_before: datetime | None,
        last_modified_time_after: datetime | None,
        last_modified_time_before: datetime | None,
        name_contains: str,
        status_equals: str,
    ) -> dict[str, Any]:
        if next_token:
            try:
                starting_index = int(next_token)
                if starting_index > len(self.processing_jobs):
                    raise ValueError  # invalid next_token
            except ValueError:
                raise AWSValidationException('Invalid pagination token because "{0}".')
        else:
            starting_index = 0

        if max_results:
            end_index = max_results + starting_index
            processing_jobs_fetched: Iterable[FakeProcessingJob] = list(
                self.processing_jobs.values()
            )[starting_index:end_index]
            if end_index >= len(self.processing_jobs):
                next_index = None
            else:
                next_index = end_index
        else:
            processing_jobs_fetched = list(self.processing_jobs.values())
            next_index = None

        if name_contains is not None:
            processing_jobs_fetched = filter(
                lambda x: name_contains in x.processing_job_name,
                processing_jobs_fetched,
            )

        if creation_time_after is not None:
            processing_jobs_fetched = filter(
                lambda x: x.creation_time > creation_time_after, processing_jobs_fetched
            )

        if creation_time_before is not None:
            processing_jobs_fetched = filter(
                lambda x: x.creation_time < creation_time_before,
                processing_jobs_fetched,
            )

        if last_modified_time_after is not None:
            processing_jobs_fetched = filter(
                lambda x: x.last_modified_time > last_modified_time_after,
                processing_jobs_fetched,
            )

        if last_modified_time_before is not None:
            processing_jobs_fetched = filter(
                lambda x: x.last_modified_time < last_modified_time_before,
                processing_jobs_fetched,
            )
        if status_equals is not None:
            processing_jobs_fetched = filter(
                lambda x: x.processing_job_status == status_equals,
                processing_jobs_fetched,
            )

        processing_job_summaries = [
            {
                "ProcessingJobName": processing_job_data.processing_job_name,
                "ProcessingJobArn": processing_job_data.arn,
                "CreationTime": processing_job_data.creation_time,
                "ProcessingEndTime": processing_job_data.processing_end_time,
                "LastModifiedTime": processing_job_data.last_modified_time,
                "ProcessingJobStatus": processing_job_data.processing_job_status,
            }
            for processing_job_data in processing_jobs_fetched
        ]

        return {
            "ProcessingJobSummaries": processing_job_summaries,
            "NextToken": str(next_index) if next_index is not None else None,
        }

    def create_transform_job(
        self,
        transform_job_name: str,
        model_name: str,
        max_concurrent_transforms: int,
        model_client_config: dict[str, int],
        max_payload_in_mb: int,
        batch_strategy: str,
        environment: dict[str, str],
        transform_input: dict[str, dict[str, str] | str],
        transform_output: dict[str, str],
        data_capture_config: dict[str, str | bool],
        transform_resources: dict[str, str | int],
        data_processing: dict[str, str],
        tags: dict[str, str],
        experiment_config: dict[str, str],
    ) -> FakeTransformJob:
        transform_job = FakeTransformJob(
            account_id=self.account_id,
            region_name=self.region_name,
            transform_job_name=transform_job_name,
            model_name=model_name,
            max_concurrent_transforms=max_concurrent_transforms,
            model_client_config=model_client_config,
            max_payload_in_mb=max_payload_in_mb,
            batch_strategy=batch_strategy,
            environment=environment,
            transform_input=transform_input,
            transform_output=transform_output,
            data_capture_config=data_capture_config,
            transform_resources=transform_resources,
            data_processing=data_processing,
            tags=tags,
            experiment_config=experiment_config,
        )
        self.transform_jobs[transform_job_name] = transform_job
        return transform_job

    def list_transform_jobs(
        self,
        next_token: str,
        max_results: int,
        creation_time_after: datetime | None,
        creation_time_before: datetime | None,
        last_modified_time_after: datetime | None,
        last_modified_time_before: datetime | None,
        name_contains: str,
        status_equals: str,
    ) -> dict[str, Any]:
        if next_token:
            try:
                starting_index = int(next_token)
                if starting_index > len(self.transform_jobs):
                    raise ValueError  # invalid next_token
            except ValueError:
                raise AWSValidationException('Invalid pagination token because "{0}".')
        else:
            starting_index = 0

        if max_results:
            end_index = max_results + starting_index
            transform_jobs_fetched: Iterable[FakeTransformJob] = list(
                self.transform_jobs.values()
            )[starting_index:end_index]
            if end_index >= len(self.transform_jobs):
                next_index = None
            else:
                next_index = end_index
        else:
            transform_jobs_fetched = list(self.transform_jobs.values())
            next_index = None

        if name_contains is not None:
            transform_jobs_fetched = filter(
                lambda x: name_contains in x.transform_job_name, transform_jobs_fetched
            )

        if creation_time_after is not None:
            transform_jobs_fetched = filter(
                lambda x: x.creation_time > creation_time_after, transform_jobs_fetched
            )

        if creation_time_before is not None:
            transform_jobs_fetched = filter(
                lambda x: x.creation_time < creation_time_before, transform_jobs_fetched
            )

        if last_modified_time_after is not None:
            transform_jobs_fetched = filter(
                lambda x: x.last_modified_time > last_modified_time_after,
                transform_jobs_fetched,
            )

        if last_modified_time_before is not None:
            transform_jobs_fetched = filter(
                lambda x: x.last_modified_time < last_modified_time_before,
                transform_jobs_fetched,
            )
        if status_equals is not None:
            transform_jobs_fetched = filter(
                lambda x: x.transform_job_status == status_equals,
                transform_jobs_fetched,
            )

        transform_job_summaries = [
            {
                "TransformJobName": transform_job_data.transform_job_name,
                "TransformJobArn": transform_job_data.arn,
                "CreationTime": transform_job_data.creation_time,
                "TransformEndTime": transform_job_data.transform_end_time,
                "LastModifiedTime": transform_job_data.last_modified_time,
                "TransformJobStatus": transform_job_data.transform_job_status,
            }
            for transform_job_data in transform_jobs_fetched
        ]

        return {
            "TransformJobSummaries": transform_job_summaries,
            "NextToken": str(next_index) if next_index is not None else None,
        }

    def describe_transform_job(self, transform_job_name: str) -> dict[str, Any]:
        try:
            return self.transform_jobs[transform_job_name].response_object
        except KeyError:
            arn = FakeTransformJob.arn_formatter(
                transform_job_name, self.account_id, self.region_name
            )
            message = f"Could not find transform job '{arn}'."
            raise ValidationError(message=message)

    def create_training_job(
        self,
        training_job_name: str,
        hyper_parameters: dict[str, str],
        algorithm_specification: dict[str, Any],
        role_arn: str,
        input_data_config: list[dict[str, Any]],
        output_data_config: dict[str, str],
        resource_config: dict[str, Any],
        vpc_config: dict[str, list[str]],
        stopping_condition: dict[str, int],
        tags: list[dict[str, str]],
        enable_network_isolation: bool,
        enable_inter_container_traffic_encryption: bool,
        enable_managed_spot_training: bool,
        checkpoint_config: dict[str, str],
        debug_hook_config: dict[str, Any],
        debug_rule_configurations: list[dict[str, Any]],
        tensor_board_output_config: dict[str, str],
        experiment_config: dict[str, str],
    ) -> FakeTrainingJob:
        training_job = FakeTrainingJob(
            account_id=self.account_id,
            region_name=self.region_name,
            training_job_name=training_job_name,
            hyper_parameters=hyper_parameters,
            algorithm_specification=algorithm_specification,
            role_arn=role_arn,
            input_data_config=input_data_config,
            output_data_config=output_data_config,
            resource_config=resource_config,
            vpc_config=vpc_config,
            stopping_condition=stopping_condition,
            tags=tags,
            enable_network_isolation=enable_network_isolation,
            enable_inter_container_traffic_encryption=enable_inter_container_traffic_encryption,
            enable_managed_spot_training=enable_managed_spot_training,
            checkpoint_config=checkpoint_config,
            debug_hook_config=debug_hook_config,
            debug_rule_configurations=debug_rule_configurations,
            tensor_board_output_config=tensor_board_output_config,
            experiment_config=experiment_config,
        )
        self.training_jobs[training_job_name] = training_job
        return training_job

    def describe_training_job(self, training_job_name: str) -> dict[str, Any]:
        try:
            return self.training_jobs[training_job_name].response_object
        except KeyError:
            arn = FakeTrainingJob.arn_formatter(
                training_job_name, self.account_id, self.region_name
            )
            message = f"Could not find training job '{arn}'."
            raise ValidationError(message=message)

    def list_training_jobs(
        self,
        next_token: str,
        max_results: int,
        creation_time_after: datetime | None,
        creation_time_before: datetime | None,
        last_modified_time_after: datetime | None,
        last_modified_time_before: datetime | None,
        name_contains: str,
        status_equals: str,
    ) -> dict[str, Any]:
        if next_token:
            try:
                starting_index = int(next_token)
                if starting_index > len(self.training_jobs):
                    raise ValueError  # invalid next_token
            except ValueError:
                raise AWSValidationException('Invalid pagination token because "{0}".')
        else:
            starting_index = 0

        if max_results:
            end_index = max_results + starting_index
            training_jobs_fetched: Iterable[FakeTrainingJob] = list(
                self.training_jobs.values()
            )[starting_index:end_index]
            if end_index >= len(self.training_jobs):
                next_index = None
            else:
                next_index = end_index
        else:
            training_jobs_fetched = list(self.training_jobs.values())
            next_index = None

        if name_contains is not None:
            training_jobs_fetched = filter(
                lambda x: name_contains in x.training_job_name, training_jobs_fetched
            )

        if creation_time_after is not None:
            training_jobs_fetched = filter(
                lambda x: x.creation_time > creation_time_after, training_jobs_fetched
            )

        if creation_time_before is not None:
            training_jobs_fetched = filter(
                lambda x: x.creation_time < creation_time_before, training_jobs_fetched
            )

        if last_modified_time_after is not None:
            training_jobs_fetched = filter(
                lambda x: x.last_modified_time > last_modified_time_after,
                training_jobs_fetched,
            )

        if last_modified_time_before is not None:
            training_jobs_fetched = filter(
                lambda x: x.last_modified_time < last_modified_time_before,
                training_jobs_fetched,
            )
        if status_equals is not None:
            training_jobs_fetched = filter(
                lambda x: x.training_job_status == status_equals, training_jobs_fetched
            )

        training_job_summaries = [
            {
                "TrainingJobName": training_job_data.training_job_name,
                "TrainingJobArn": training_job_data.arn,
                "CreationTime": training_job_data.creation_time,
                "TrainingEndTime": training_job_data.training_end_time,
                "LastModifiedTime": training_job_data.last_modified_time,
                "TrainingJobStatus": training_job_data.training_job_status,
            }
            for training_job_data in training_jobs_fetched
        ]

        return {
            "TrainingJobSummaries": training_job_summaries,
            "NextToken": str(next_index) if next_index is not None else None,
        }

    def update_endpoint_weights_and_capacities(
        self, endpoint_name: str, desired_weights_and_capacities: list[dict[str, Any]]
    ) -> str:
        # Validate inputs
        endpoint = self.endpoints.get(endpoint_name, None)
        if not endpoint:
            arn = FakeEndpoint.arn_formatter(
                endpoint_name, self.account_id, self.region_name
            )
            raise AWSValidationException(f'Could not find endpoint "{arn}".')

        names_checked = []
        for variant_config in desired_weights_and_capacities:
            name = variant_config.get("VariantName")

            if name in names_checked:
                raise AWSValidationException(
                    f'The variant name "{name}" was non-unique within the request.'
                )

            if not any(
                variant["VariantName"] == name
                for variant in endpoint.production_variants
            ):
                raise AWSValidationException(
                    f'The variant name(s) "{name}" is/are not present within endpoint configuration "{endpoint.endpoint_config_name}".'
                )

            names_checked.append(name)

        # Update endpoint variants
        endpoint.endpoint_status = "Updating"

        for variant_config in desired_weights_and_capacities:
            name = variant_config.get("VariantName")
            desired_weight = variant_config.get("DesiredWeight")
            desired_instance_count = variant_config.get("DesiredInstanceCount")

            for variant in endpoint.production_variants:
                if variant.get("VariantName") == name:
                    variant["DesiredWeight"] = desired_weight
                    variant["CurrentWeight"] = desired_weight
                    variant["DesiredInstanceCount"] = desired_instance_count
                    variant["CurrentInstanceCount"] = desired_instance_count
                    break

        endpoint.endpoint_status = "InService"
        return endpoint.arn

    def create_model_package_group(
        self,
        model_package_group_name: str,
        model_package_group_description: str,
        tags: list[dict[str, str]] | None = None,
    ) -> str:
        self.model_package_groups[model_package_group_name] = ModelPackageGroup(
            model_package_group_name=model_package_group_name,
            model_package_group_description=model_package_group_description,
            account_id=self.account_id,
            region_name=self.region_name,
            tags=tags or [],
        )
        return self.model_package_groups[model_package_group_name].arn

    def _get_versioned_or_not(
        self, model_package_type: str | None, model_package_version: int | None
    ) -> bool:
        if model_package_type == "Versioned":
            return model_package_version is not None
        elif model_package_type == "Unversioned" or model_package_type is None:
            return model_package_version is None
        elif model_package_type == "Both":
            return True
        raise ValueError(f"Invalid model package type: {model_package_type}")

    @paginate(pagination_model=PAGINATION_MODEL)
    def list_model_package_groups(
        self,
        creation_time_after: datetime | None,
        creation_time_before: datetime | None,
        name_contains: str | None,
        sort_by: str | None,
        sort_order: str | None,
    ) -> list[ModelPackageGroup]:
        model_package_group_summary_list = list(
            filter(
                lambda x: (
                    creation_time_after is None or x.creation_time > creation_time_after
                )
                and (
                    creation_time_before is None
                    or x.creation_time < creation_time_before
                )
                and (
                    name_contains is None
                    or x.model_package_group_name.find(name_contains) != -1
                ),
                self.model_package_groups.values(),
            )
        )
        model_package_group_summary_list = sorted(
            model_package_group_summary_list,
            key={
                "Name": lambda x: x.model_package_group_name,
                "CreationTime": lambda x: x.creation_time,
                None: lambda x: x.creation_time,
            }[sort_by],
            reverse=sort_order == "Descending",
        )
        return model_package_group_summary_list

    def describe_model_package_group(
        self, model_package_group_name: str
    ) -> ModelPackageGroup:
        model_package_group = self.model_package_groups.get(model_package_group_name)
        if model_package_group is None:
            model_package_group_arn = arn_formatter(
                region_name=self.region_name,
                account_id=self.account_id,
                _type="model-package-group",
                _id=f"{model_package_group_name}",
            )
            raise ValidationError(
                f"ModelPackageGroup {model_package_group_arn} does not exist."
            )
        return model_package_group

    @paginate(pagination_model=PAGINATION_MODEL)
    def list_model_packages(
        self,
        creation_time_after: datetime | None,
        creation_time_before: datetime | None,
        name_contains: str | None,
        model_approval_status: str | None,
        model_package_group_name: str | None,
        model_package_type: str | None,
        sort_by: str | None,
        sort_order: str | None,
    ) -> list[ModelPackage]:
        if model_package_group_name is not None:
            model_package_type = "Versioned"
            if re.match(ARN_PARTITION_REGEX, model_package_group_name):
                model_package_group_name = model_package_group_name.split("/")[-1]
        model_package_summary_list = list(
            filter(
                lambda x: (
                    creation_time_after is None or x.creation_time > creation_time_after
                )
                and (
                    creation_time_before is None
                    or x.creation_time < creation_time_before
                )
                and (
                    name_contains is None
                    or x.model_package_name.find(name_contains) != -1
                )
                and (
                    model_approval_status is None
                    or x.model_approval_status == model_approval_status
                )
                and (
                    model_package_group_name is None
                    or x.model_package_group_name == model_package_group_name
                )
                and self._get_versioned_or_not(
                    model_package_type, x.model_package_version
                ),
                self.model_packages.values(),
            )
        )
        model_package_summary_list = sorted(
            model_package_summary_list,
            key={
                "Name": lambda x: x.model_package_name,
                "CreationTime": lambda x: x.creation_time,
                None: lambda x: x.creation_time,
            }[sort_by],
            reverse=sort_order == "Descending",
        )
        return model_package_summary_list

    def describe_model_package(self, model_package_name: str) -> ModelPackage:
        model_package_name_mapped = self.model_package_name_mapping.get(
            model_package_name, model_package_name
        )
        model_package = self.model_packages.get(model_package_name_mapped)
        if model_package is None:
            raise ValidationError(f"Model package {model_package_name} not found")
        return model_package

    def update_model_package(
        self,
        model_package_arn: str,
        model_approval_status: str | None,
        approval_description: str | None,
        customer_metadata_properties: dict[str, str] | None,
        customer_metadata_properties_to_remove: list[str],
        additional_inference_specifications_to_add: list[Any] | None,
    ) -> str:
        model_package_name_mapped = self.model_package_name_mapping.get(
            model_package_arn, model_package_arn
        )
        model_package = self.model_packages.get(model_package_name_mapped)

        if model_package is None:
            raise ValidationError(f"Model package {model_package_arn} not found")

        model_package.set_model_approval_status(model_approval_status)
        model_package.approval_description = approval_description
        model_package.customer_metadata_properties = customer_metadata_properties
        model_package.remove_customer_metadata_property(
            customer_metadata_properties_to_remove
        )
        model_package.add_additional_inference_specifications(
            additional_inference_specifications_to_add
        )
        model_package.modifications_done()

        return model_package.arn

    def create_model_package(
        self,
        model_package_name: str | None,
        model_package_group_name: str | None,
        model_package_description: str | None,
        inference_specification: Any,
        validation_specification: Any,
        source_algorithm_specification: Any,
        certify_for_marketplace: bool,
        tags: Any,
        model_approval_status: str | None,
        metadata_properties: Any,
        model_metrics: Any,
        client_token: Any,
        customer_metadata_properties: Any,
        drift_check_baselines: Any,
        domain: Any,
        task: Any,
        sample_payload_url: Any,
        additional_inference_specifications: Any,
    ) -> str:
        model_package_version = None
        if model_package_group_name and model_package_name:
            raise AWSValidationException(
                "An error occurred (ValidationException) when calling the CreateModelPackage operation: Both ModelPackageName and ModelPackageGroupName are provided in the input. Cannot determine which one to use."
            )
        elif not model_package_group_name and not model_package_name:
            raise AWSValidationException(
                "An error ocurred (ValidationException) when calling the CreateModelPackag operation: Missing ARN."
            )
        elif model_package_group_name:
            model_package_type = "Versioned"
            model_package_name = model_package_group_name
            model_packages_for_group = [
                x
                for x in self.model_packages.values()
                if x.model_package_group_name == model_package_group_name
            ]
            if model_package_group_name not in self.model_package_groups:
                raise AWSValidationException(
                    "An error ocurred (ValidationException) when calling the CreateModelPackage operation: Model Package Group does not exist."
                )
            model_package_version = len(model_packages_for_group) + 1
        else:
            model_package_type = "Unversioned"

        model_package = ModelPackage(
            model_package_name=cast(str, model_package_name),
            model_package_group_name=model_package_group_name,
            model_package_description=model_package_description,
            inference_specification=inference_specification,
            validation_specification=validation_specification,
            source_algorithm_specification=source_algorithm_specification,
            certify_for_marketplace=certify_for_marketplace,
            tags=tags,
            model_approval_status=model_approval_status,
            metadata_properties=metadata_properties,
            model_metrics=model_metrics,
            customer_metadata_properties=customer_metadata_properties,
            drift_check_baselines=drift_check_baselines,
            domain=domain,
            task=task,
            sample_payload_url=sample_payload_url,
            additional_inference_specifications=additional_inference_specifications,
            model_package_version=model_package_version,
            approval_description=None,
            region_name=self.region_name,
            account_id=self.account_id,
            client_token=client_token,
            model_package_type=model_package_type,
        )
        self.model_package_name_mapping[model_package.model_package_name] = (
            model_package.arn
        )
        self.model_package_name_mapping[model_package.arn] = model_package.arn
        self.model_packages[model_package.arn] = model_package
        return model_package.arn

    def create_feature_group(
        self,
        feature_group_name: str,
        record_identifier_feature_name: str,
        event_time_feature_name: str,
        feature_definitions: list[dict[str, str]],
        offline_store_config: dict[str, Any],
        role_arn: str,
        tags: Any,
    ) -> str:
        feature_group_arn = arn_formatter(
            region_name=self.region_name,
            account_id=self.account_id,
            _type="feature-group",
            _id=f"{feature_group_name.lower()}",
        )
        if feature_group_arn in self.feature_groups:
            raise ResourceInUseException(
                message=f"An error occurred (ResourceInUse) when calling the CreateFeatureGroup operation: Resource Already Exists: FeatureGroup with name {feature_group_name} already exists. Choose a different name.\nInfo: Feature Group '{feature_group_name}' already exists."
            )

        feature_group = FeatureGroup(
            feature_group_name=feature_group_name,
            record_identifier_feature_name=record_identifier_feature_name,
            event_time_feature_name=event_time_feature_name,
            feature_definitions=feature_definitions,
            offline_store_config=offline_store_config,
            role_arn=role_arn,
            region_name=self.region_name,
            account_id=self.account_id,
            tags=tags,
        )
        self.feature_groups[feature_group.arn] = feature_group
        return feature_group.arn

    def describe_feature_group(
        self,
        feature_group_name: str,
    ) -> dict[str, Any]:
        feature_group_arn = arn_formatter(
            region_name=self.region_name,
            account_id=self.account_id,
            _type="feature-group",
            _id=f"{feature_group_name.lower()}",
        )

        feature_group = self.feature_groups[feature_group_arn]
        return feature_group.describe()

    def create_cluster(
        self,
        cluster_name: str,
        instance_groups: list[dict[str, Any]],
        vpc_config: dict[str, list[str]],
        tags: Any,
    ) -> str:
        cluster = Cluster(
            cluster_name=cluster_name,
            region_name=self.region_name,
            account_id=self.account_id,
            instance_groups=instance_groups,
            vpc_config=vpc_config,
            tags=tags,
        )
        self.clusters[cluster_name] = cluster

        # create Cluster Nodes
        for instance_group in instance_groups:
            for i in range(instance_group["TargetCount"]):
                node_id = f"{instance_group['InstanceGroupName']}-{i}"
                fake_cluster_node = ClusterNode(
                    region_name=self.region_name,
                    account_id=self.account_id,
                    cluster_name=cluster_name,
                    instance_group_name=instance_group["InstanceGroupName"],
                    instance_type=instance_group["InstanceType"],
                    life_cycle_config=instance_group["LifeCycleConfig"],
                    execution_role=instance_group["ExecutionRole"],
                    node_id=node_id,
                    threads_per_core=instance_group["ThreadsPerCore"],
                )
                cluster.nodes[node_id] = fake_cluster_node

        return cluster.arn

    def describe_cluster(self, cluster_name: str) -> dict[str, Any]:
        if cluster_name.startswith(f"arn:{self.partition}:sagemaker:"):
            cluster_name = (cluster_name.split(":")[-1]).split("/")[-1]
        cluster = self.clusters.get(cluster_name)
        if not cluster:
            raise ValidationError(message=f"Could not find cluster '{cluster_name}'.")
        return cluster.describe()

    def delete_cluster(self, cluster_name: str) -> str:
        if cluster_name.startswith(f"arn:{self.partition}:sagemaker:"):
            cluster_name = (cluster_name.split(":")[-1]).split("/")[-1]
        cluster = self.clusters.get(cluster_name)
        if not cluster:
            raise ValidationError(message=f"Could not find cluster '{cluster_name}'.")
        arn = cluster.arn

        del self.clusters[cluster_name]
        return arn

    def describe_cluster_node(self, cluster_name: str, node_id: str) -> dict[str, Any]:
        if cluster_name.startswith(f"arn:{self.partition}:sagemaker:"):
            cluster_name = (cluster_name.split(":")[-1]).split("/")[-1]
        cluster = self.clusters.get(cluster_name)
        if not cluster:
            raise ValidationError(message=f"Could not find cluster '{cluster_name}'.")
        if node_id in cluster.nodes:
            return cluster.nodes[node_id].describe()
        else:
            raise ValidationError(
                message=f"Could not find node '{node_id}' in cluster '{cluster_name}'."
            )

    @paginate(pagination_model=PAGINATION_MODEL)
    def list_clusters(
        self,
        creation_time_after: datetime | None,
        creation_time_before: datetime | None,
        name_contains: str | None,
        sort_by: str | None,
        sort_order: str | None,
    ) -> list[Cluster]:
        clusters = list(self.clusters.values())
        if name_contains:
            clusters = [i for i in clusters if name_contains in i.cluster_name]
        if creation_time_before:
            clusters = [i for i in clusters if i.creation_time < creation_time_before]
        if creation_time_after:
            clusters = [i for i in clusters if i.creation_time > creation_time_after]
        reverse = sort_order == "Descending"
        if sort_by == "Name":
            clusters = sorted(clusters, key=lambda x: x.cluster_name, reverse=reverse)
        if sort_by == "CreationTime" or sort_by is None:
            clusters = sorted(clusters, key=lambda x: x.creation_time, reverse=reverse)
        return clusters

    @paginate(pagination_model=PAGINATION_MODEL)
    def list_cluster_nodes(
        self,
        cluster_name: str,
        creation_time_after: datetime | None,
        creation_time_before: datetime | None,
        instance_group_name_contains: str | None,
        sort_by: str | None,
        sort_order: str | None,
    ) -> list[ClusterNode]:
        if cluster_name.startswith(f"arn:{self.partition}:sagemaker:"):
            cluster_name = (cluster_name.split(":")[-1]).split("/")[-1]
        cluster = self.clusters.get(cluster_name)
        if not cluster:
            raise ValidationError(message=f"Could not find cluster '{cluster_name}'.")
        nodes_list = list(cluster.nodes.values())

        if instance_group_name_contains:
            nodes_list = [
                i
                for i in nodes_list
                if instance_group_name_contains in i.instance_group_name
            ]
        if creation_time_before:
            nodes_list = [i for i in nodes_list if i.launch_time < creation_time_before]
        if creation_time_after:
            nodes_list = [i for i in nodes_list if i.launch_time > creation_time_after]
        reverse = sort_order == "Descending"
        if sort_by == "Name":
            nodes_list = sorted(
                nodes_list, key=lambda x: x.instance_group_name, reverse=reverse
            )
        if sort_by == "CreationTime" or sort_by is None:
            nodes_list = sorted(
                nodes_list, key=lambda x: x.launch_time, reverse=reverse
            )
        return nodes_list

    def create_model_bias_job_definition(
        self,
        account_id: str,
        job_definition_name: str,
        tags: list[dict[str, str]] | None = None,
        role_arn: str = "",
        job_resources: dict[str, Any] | None = None,
        stopping_condition: dict[str, Any] | None = None,
        environment: dict[str, str] | None = None,
        network_config: dict[str, Any] | None = None,
        model_bias_baseline_config: dict[str, Any] | None = None,
        model_bias_app_specification: dict[str, Any] | None = None,
        model_bias_job_input: dict[str, Any] | None = None,
        model_bias_job_output_config: dict[str, Any] | None = None,
    ) -> dict[str, str]:
        job_definition = FakeModelBiasJobDefinition(
            account_id=account_id,
            region_name=self.region_name,
            job_definition_name=job_definition_name,
            tags=tags or [],
            role_arn=role_arn,
            job_resources=job_resources,
            stopping_condition=stopping_condition,
            environment=environment,
            network_config=network_config,
            model_bias_baseline_config=model_bias_baseline_config,
            model_bias_app_specification=model_bias_app_specification,
            model_bias_job_input=model_bias_job_input,
            model_bias_job_output_config=model_bias_job_output_config,
        )
        self.model_bias_job_definitions[job_definition_name] = job_definition
        return job_definition.response_create

    @paginate(pagination_model=PAGINATION_MODEL)
    def list_model_bias_job_definitions(self) -> list[dict[str, str]]:
        return [job.summary_object for job in self.model_bias_job_definitions.values()]

    def describe_model_bias_job_definition(
        self, job_definition_name: str
    ) -> dict[str, Any]:
        job_definition = self.model_bias_job_definitions.get(job_definition_name)
        if job_definition is None:
            raise ResourceNotFound(f"Job definition {job_definition_name} not found")
        return job_definition.response_object

    def delete_model_bias_job_definition(self, job_definition_name: str) -> None:
        if job_definition_name in self.model_bias_job_definitions:
            del self.model_bias_job_definitions[job_definition_name]
        else:
            raise ResourceNotFound(f"Job definition {job_definition_name} not found")

    def create_auto_ml_job_v2(
        self,
        auto_ml_job_name: str,
        auto_ml_job_input_data_config: list[dict[str, Any]],
        output_data_config: dict[str, Any],
        auto_ml_problem_type_config: dict[str, Any],
        role_arn: str,
        tags: list[dict[str, str]] | None,
        security_config: dict[str, Any] | None,
        auto_ml_job_objective: dict[str, str] | None,
        model_deploy_config: dict[str, Any] | None,
        data_split_config: dict[str, Any] | None,
    ) -> str:
        auto_ml_job = AutoMLJob(
            auto_ml_job_name=auto_ml_job_name,
            auto_ml_job_input_data_config=auto_ml_job_input_data_config,
            output_data_config=output_data_config,
            auto_ml_problem_type_config=auto_ml_problem_type_config,
            role_arn=role_arn,
            region_name=self.region_name,
            account_id=self.account_id,
            tags=tags,
            security_config=security_config,
            auto_ml_job_objective=auto_ml_job_objective,
            model_deploy_config=model_deploy_config,
            data_split_config=data_split_config,
        )

        self.auto_ml_jobs[auto_ml_job_name] = auto_ml_job
        return auto_ml_job.arn

    def describe_auto_ml_job_v2(self, auto_ml_job_name: str) -> dict[str, Any]:
        if auto_ml_job_name not in self.auto_ml_jobs:
            raise ResourceNotFound(
                f"Could not find AutoML job with name {auto_ml_job_name}."
            )
        auto_ml_job = self.auto_ml_jobs[auto_ml_job_name]
        return auto_ml_job.describe()

    @paginate(pagination_model=PAGINATION_MODEL)
    def list_auto_ml_jobs(
        self,
        creation_time_after: datetime | None,
        creation_time_before: datetime | None,
        last_modified_time_after: datetime | None,
        last_modified_time_before: datetime | None,
        name_contains: str | None,
        status_equals: str | None,
        sort_order: str | None,
        sort_by: str | None,
    ) -> list[AutoMLJob]:
        auto_ml_jobs = list(self.auto_ml_jobs.values())
        if name_contains:
            auto_ml_jobs = [
                i for i in auto_ml_jobs if name_contains in i.auto_ml_job_name
            ]
        if status_equals:
            auto_ml_jobs = [
                i for i in auto_ml_jobs if status_equals == i.auto_ml_job_status
            ]
        if creation_time_before:
            auto_ml_jobs = [
                i for i in auto_ml_jobs if i.creation_time < creation_time_before
            ]
        if creation_time_after:
            auto_ml_jobs = [
                i for i in auto_ml_jobs if i.creation_time > creation_time_after
            ]
        if last_modified_time_before:
            auto_ml_jobs = [
                i
                for i in auto_ml_jobs
                if i.last_modified_time < last_modified_time_before
            ]
        if last_modified_time_after:
            auto_ml_jobs = [
                i
                for i in auto_ml_jobs
                if i.last_modified_time > last_modified_time_after
            ]
        reverse = sort_order == "Descending"
        if sort_by == "Status":
            auto_ml_jobs = sorted(
                auto_ml_jobs, key=lambda x: x.auto_ml_job_status, reverse=reverse
            )
        if sort_by == "CreationTime":
            auto_ml_jobs = sorted(
                auto_ml_jobs, key=lambda x: x.creation_time, reverse=reverse
            )
        if sort_by == "Name" or sort_by is None:
            auto_ml_jobs = sorted(
                auto_ml_jobs, key=lambda x: x.auto_ml_job_name, reverse=reverse
            )
        return auto_ml_jobs

    def stop_auto_ml_job(self, auto_ml_job_name: str) -> None:
        if auto_ml_job_name not in self.auto_ml_jobs:
            raise ResourceNotFound(
                f"Could not find AutoML job with name {auto_ml_job_name}."
            )
        auto_ml_job = self.auto_ml_jobs[auto_ml_job_name]
        auto_ml_job.auto_ml_job_status = "Stopped"
        auto_ml_job.auto_ml_job_secondary_status = "Stopped"

    @paginate(pagination_model=PAGINATION_MODEL)
    def list_endpoints(
        self,
        sort_by: str | None,
        sort_order: str | None,
        name_contains: str | None,
        creation_time_before: datetime | None,
        creation_time_after: datetime | None,
        last_modified_time_before: datetime | None,
        last_modified_time_after: datetime | None,
        status_equals: str | None,
    ) -> list[FakeEndpoint]:
        endpoints = list(self.endpoints.values())
        if name_contains:
            endpoints = [i for i in endpoints if name_contains in i.endpoint_name]
        if status_equals:
            endpoints = [i for i in endpoints if status_equals == i.endpoint_status]
        if creation_time_before:
            endpoints = [i for i in endpoints if i.creation_time < creation_time_before]
        if creation_time_after:
            endpoints = [i for i in endpoints if i.creation_time > creation_time_after]
        if last_modified_time_before:
            endpoints = [
                i for i in endpoints if i.last_modified_time < last_modified_time_before
            ]
        if last_modified_time_after:
            endpoints = [
                i for i in endpoints if i.last_modified_time > last_modified_time_after
            ]
        reverse = sort_order == "Descending"
        if sort_by == "Name":
            endpoints = sorted(
                endpoints, key=lambda x: x.endpoint_name, reverse=reverse
            )
        elif sort_by == "Status":
            endpoints = sorted(
                endpoints, key=lambda x: x.endpoint_status, reverse=reverse
            )
        else:
            endpoints = sorted(
                endpoints, key=lambda x: x.creation_time, reverse=reverse
            )
        return endpoints

    @paginate(pagination_model=PAGINATION_MODEL)
    def list_endpoint_configs(
        self,
        sort_by: str | None,
        sort_order: str | None,
        name_contains: str | None,
        creation_time_before: datetime | None,
        creation_time_after: datetime | None,
    ) -> list[FakeEndpointConfig]:
        endpoint_configs = list(self.endpoint_configs.values())
        if name_contains:
            endpoint_configs = [
                i for i in endpoint_configs if name_contains in i.endpoint_config_name
            ]
        if creation_time_before:
            endpoint_configs = [
                i for i in endpoint_configs if i.creation_time < creation_time_before
            ]
        if creation_time_after:
            endpoint_configs = [
                i for i in endpoint_configs if i.creation_time > creation_time_after
            ]
        reverse = sort_order == "Descending"
        if sort_by == "Name":
            endpoint_configs = sorted(
                endpoint_configs, key=lambda x: x.endpoint_config_name, reverse=reverse
            )
        else:
            endpoint_configs = sorted(
                endpoint_configs, key=lambda x: x.creation_time, reverse=reverse
            )
        return endpoint_configs

    def create_compilation_job(
        self,
        compilation_job_name: str,
        role_arn: str,
        output_config: dict[str, Any],
        stopping_condition: dict[str, Any],
        model_package_version_arn: str | None,
        input_config: dict[str, Any] | None,
        vpc_config: dict[str, Any] | None,
        tags: list[dict[str, str]] | None,
    ) -> str:
        compilation_job = CompilationJob(
            compilation_job_name=compilation_job_name,
            role_arn=role_arn,
            region_name=self.region_name,
            account_id=self.account_id,
            model_package_version_arn=model_package_version_arn,
            input_config=input_config,
            output_config=output_config,
            vpc_config=vpc_config,
            stopping_condition=stopping_condition,
            tags=tags,
        )
        self.compilation_jobs[compilation_job_name] = compilation_job
        return compilation_job.arn

    def describe_compilation_job(self, compilation_job_name: str) -> dict[str, Any]:
        if compilation_job_name not in self.compilation_jobs:
            raise ResourceNotFound(
                message=f"Could not find compilation job '{compilation_job_name}'."
            )
        compilation_job = self.compilation_jobs[compilation_job_name]
        return compilation_job.describe()

    @paginate(pagination_model=PAGINATION_MODEL)
    def list_compilation_jobs(
        self,
        creation_time_after: datetime | None,
        creation_time_before: datetime | None,
        last_modified_time_after: datetime | None,
        last_modified_time_before: datetime | None,
        name_contains: str | None,
        status_equals: str | None,
        sort_by: str | None,
        sort_order: str | None,
    ) -> list[CompilationJob]:
        compilation_jobs = list(self.compilation_jobs.values())
        if name_contains:
            compilation_jobs = [
                i for i in compilation_jobs if name_contains in i.compilation_job_name
            ]
        if creation_time_before:
            compilation_jobs = [
                i for i in compilation_jobs if i.creation_time < creation_time_before
            ]
        if creation_time_after:
            compilation_jobs = [
                i for i in compilation_jobs if i.creation_time > creation_time_after
            ]
        if last_modified_time_before:
            compilation_jobs = [
                i
                for i in compilation_jobs
                if i.last_modified_time < last_modified_time_before
            ]
        if last_modified_time_after:
            compilation_jobs = [
                i
                for i in compilation_jobs
                if i.last_modified_time > last_modified_time_after
            ]
        if status_equals:
            compilation_jobs = [
                i for i in compilation_jobs if i.compilation_job_status == status_equals
            ]
        reverse = sort_order == "Descending"
        if sort_by == "Name":
            compilation_jobs = sorted(
                compilation_jobs, key=lambda x: x.compilation_job_name, reverse=reverse
            )
        if sort_by == "Status":
            compilation_jobs = sorted(
                compilation_jobs,
                key=lambda x: x.compilation_job_status,
                reverse=reverse,
            )
        if sort_by == "CreationTime" or sort_by is None:
            compilation_jobs = sorted(
                compilation_jobs, key=lambda x: x.creation_time, reverse=reverse
            )
        return compilation_jobs

    def delete_compilation_job(self, compilation_job_name: str) -> None:
        if compilation_job_name not in self.compilation_jobs:
            raise ResourceNotFound(
                message=f"Could not find compilation job '{compilation_job_name}'."
            )
        del self.compilation_jobs[compilation_job_name]

    def create_domain(
        self,
        domain_name: str,
        auth_mode: str,
        default_user_settings: dict[str, Any],
        subnet_ids: list[str],
        vpc_id: str,
        domain_settings: dict[str, Any] | None,
        tags: list[dict[str, str]] | None,
        app_network_access_type: str | None,
        home_efs_file_system_kms_key_id: str | None,
        kms_key_id: str | None,
        app_security_group_management: str | None,
        default_space_settings: dict[str, Any] | None,
    ) -> dict[str, Any]:
        domain = Domain(
            domain_name=domain_name,
            auth_mode=auth_mode,
            default_user_settings=default_user_settings,
            subnet_ids=subnet_ids,
            vpc_id=vpc_id,
            domain_settings=domain_settings,
            tags=tags,
            app_network_access_type=app_network_access_type,
            home_efs_file_system_kms_key_id=home_efs_file_system_kms_key_id,
            kms_key_id=kms_key_id,
            app_security_group_management=app_security_group_management,
            default_space_settings=default_space_settings,
            region_name=self.region_name,
            account_id=self.account_id,
        )
        self.domains[domain.id] = domain
        return {"DomainArn": domain.arn, "Url": domain.url}

    def describe_domain(self, domain_id: str) -> dict[str, Any]:
        if domain_id not in self.domains:
            raise ValidationError(message=f"Could not find domain '{domain_id}'.")
        return self.domains[domain_id].describe()

    @paginate(pagination_model=PAGINATION_MODEL)
    def list_domains(self) -> list[Domain]:
        return list(self.domains.values())

    def delete_domain(
        self, domain_id: str, retention_policy: dict[str, str] | None
    ) -> None:
        # 'retention_policy' parameter is not used
        if domain_id not in self.domains:
            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: dict[str, Any] | None,
        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: dict[str, Any] | None,
        role_arn: str,
        stopping_condition: dict[str, Any] | None,
        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: str | None,
        sort_by: str | None,
        sort_order: str | None,
        name_contains: str | None,
        creation_time_before: datetime | None,
        creation_time_after: datetime | None,
    ) -> 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 < creation_time_before
            ]
        if creation_time_after:
            model_explainability_job_definitions = [
                i
                for i in model_explainability_job_definitions
                if i.creation_time > 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]

    def create_hyper_parameter_tuning_job(
        self,
        hyper_parameter_tuning_job_name: str,
        hyper_parameter_tuning_job_config: dict[str, Any],
        training_job_definition: dict[str, Any] | None,
        training_job_definitions: list[dict[str, Any]] | None,
        warm_start_config: dict[str, Any] | None,
        tags: list[dict[str, str]] | None,
        autotune: dict[str, Any] | None,
    ) -> str:
        hyper_parameter_tuning_job = HyperParameterTuningJob(
            hyper_parameter_tuning_job_name=hyper_parameter_tuning_job_name,
            hyper_parameter_tuning_job_config=hyper_parameter_tuning_job_config,
            region_name=self.region_name,
            account_id=self.account_id,
            training_job_definition=training_job_definition,
            training_job_definitions=training_job_definitions,
            warm_start_config=warm_start_config,
            tags=tags,
            autotune=autotune,
        )

        self.hyper_parameter_tuning_jobs[hyper_parameter_tuning_job_name] = (
            hyper_parameter_tuning_job
        )
        return hyper_parameter_tuning_job.arn

    def describe_hyper_parameter_tuning_job(
        self, hyper_parameter_tuning_job_name: str
    ) -> dict[str, Any]:
        if hyper_parameter_tuning_job_name not in self.hyper_parameter_tuning_jobs:
            raise ResourceNotFound(
                message=f"Could not find hyper parameter tuning job '{hyper_parameter_tuning_job_name}'."
            )
        return self.hyper_parameter_tuning_jobs[
            hyper_parameter_tuning_job_name
        ].describe()

    @paginate(pagination_model=PAGINATION_MODEL)
    def list_hyper_parameter_tuning_jobs(
        self,
        sort_by: str | None,
        sort_order: str | None,
        name_contains: str | None,
        creation_time_after: datetime | None,
        creation_time_before: datetime | None,
        last_modified_time_after: datetime | None,
        last_modified_time_before: datetime | None,
        status_equals: str | None,
    ) -> list[HyperParameterTuningJob]:
        hyper_parameter_tuning_jobs = list(self.hyper_parameter_tuning_jobs.values())
        if name_contains:
            hyper_parameter_tuning_jobs = [
                i
                for i in hyper_parameter_tuning_jobs
                if name_contains in i.hyper_parameter_tuning_job_name
            ]
        if status_equals:
            hyper_parameter_tuning_jobs = [
                i
                for i in hyper_parameter_tuning_jobs
                if status_equals == i.hyper_parameter_tuning_job_status
            ]
        if creation_time_before:
            hyper_parameter_tuning_jobs = [
                i
                for i in hyper_parameter_tuning_jobs
                if i.creation_time < creation_time_before
            ]
        if creation_time_after:
            hyper_parameter_tuning_jobs = [
                i
                for i in hyper_parameter_tuning_jobs
                if i.creation_time > creation_time_after
            ]
        if last_modified_time_before:
            hyper_parameter_tuning_jobs = [
                i
                for i in hyper_parameter_tuning_jobs
                if i.last_modified_time < last_modified_time_before
            ]
        if last_modified_time_after:
            hyper_parameter_tuning_jobs = [
                i
                for i in hyper_parameter_tuning_jobs
                if i.last_modified_time > last_modified_time_after
            ]
        reverse = sort_order == "Descending"
        if sort_by == "Name":
            hyper_parameter_tuning_jobs = sorted(
                hyper_parameter_tuning_jobs,
                key=lambda x: x.hyper_parameter_tuning_job_name,
                reverse=reverse,
            )
        elif sort_by == "Status":
            hyper_parameter_tuning_jobs = sorted(
                hyper_parameter_tuning_jobs,
                key=lambda x: x.hyper_parameter_tuning_job_status,
                reverse=reverse,
            )
        else:
            hyper_parameter_tuning_jobs = sorted(
                hyper_parameter_tuning_jobs,
                key=lambda x: x.creation_time,
                reverse=reverse,
            )
        return hyper_parameter_tuning_jobs

    def delete_hyper_parameter_tuning_job(
        self, hyper_parameter_tuning_job_name: str
    ) -> None:
        if hyper_parameter_tuning_job_name not in self.hyper_parameter_tuning_jobs:
            raise ResourceNotFound(
                message=f"Could not find hyper parameter tuning job '{hyper_parameter_tuning_job_name}'."
            )
        del self.hyper_parameter_tuning_jobs[hyper_parameter_tuning_job_name]

    def create_model_quality_job_definition(
        self,
        job_definition_name: str,
        model_quality_baseline_config: dict[str, Any] | None,
        model_quality_app_specification: dict[str, Any],
        model_quality_job_input: dict[str, Any],
        model_quality_job_output_config: dict[str, Any],
        job_resources: dict[str, Any],
        network_config: dict[str, Any] | None,
        role_arn: str,
        stopping_condition: dict[str, Any] | None,
        tags: list[dict[str, str]] | None,
    ) -> str:
        model_quality_job_definition = ModelQualityJobDefinition(
            job_definition_name=job_definition_name,
            model_quality_baseline_config=model_quality_baseline_config,
            model_quality_app_specification=model_quality_app_specification,
            model_quality_job_input=model_quality_job_input,
            model_quality_job_output_config=model_quality_job_output_config,
            job_resources=job_resources,
            network_config=network_config,
            role_arn=role_arn,
            stopping_condition=stopping_condition,
            region_name=self.region_name,
            account_id=self.account_id,
            tags=tags,
        )
        self.model_quality_job_definitions[job_definition_name] = (
            model_quality_job_definition
        )
        return model_quality_job_definition.arn

    def describe_model_quality_job_definition(
        self, job_definition_name: str
    ) -> dict[str, Any]:
        if job_definition_name not in self.model_quality_job_definitions:
            raise ResourceNotFound(
                message=f"Could not find model quality job definition '{job_definition_name}'."
            )
        return self.model_quality_job_definitions[job_definition_name].describe()

    @paginate(pagination_model=PAGINATION_MODEL)
    def list_model_quality_job_definitions(
        self,
        endpoint_name: str | None,
        sort_by: str | None,
        sort_order: str | None,
        name_contains: str | None,
        creation_time_before: datetime | None,
        creation_time_after: datetime | None,
    ) -> list[ModelQualityJobDefinition]:
        model_quality_job_definitions = list(
            self.model_quality_job_definitions.values()
        )
        if endpoint_name:
            model_quality_job_definitions = [
                i
                for i in model_quality_job_definitions
                if endpoint_name == i.endpoint_name
            ]
        if name_contains:
            model_quality_job_definitions = [
                i
                for i in model_quality_job_definitions
                if name_contains in i.job_definition_name
            ]
        if creation_time_before:
            model_quality_job_definitions = [
                i
                for i in model_quality_job_definitions
                if i.creation_time < creation_time_before
            ]
        if creation_time_after:
            model_quality_job_definitions = [
                i
                for i in model_quality_job_definitions
                if i.creation_time > creation_time_after
            ]
        reverse = sort_order == "Descending"
        if sort_by == "Name":
            model_quality_job_definitions = sorted(
                model_quality_job_definitions,
                key=lambda x: x.job_definition_name,
                reverse=reverse,
            )
        if sort_by == "CreationTime" or sort_by is None:
            model_quality_job_definitions = sorted(
                model_quality_job_definitions,
                key=lambda x: x.creation_time,
                reverse=reverse,
            )
        return model_quality_job_definitions

    def delete_model_quality_job_definition(self, job_definition_name: str) -> None:
        if job_definition_name not in self.model_quality_job_definitions:
            raise ResourceNotFound(
                message=f"Could not find model quality job definition '{job_definition_name}'."
            )
        del self.model_quality_job_definitions[job_definition_name]

    def create_model_card(
        self,
        model_card_name: str,
        security_config: dict[str, str] | None,
        content: str,
        model_card_status: str,
        tags: list[dict[str, str]] | None,
        model_card_version: int | None = None,
        creation_time: datetime | None = None,
        last_modified_time: datetime | None = None,
    ) -> str:
        if model_card_name in self.model_cards:
            raise ConflictException(f"Modelcard {model_card_name} already exists")

        if not model_card_version:
            model_card_version = 1

        # implement here
        model_card = FakeModelCard(
            account_id=self.account_id,
            region_name=self.region_name,
            model_card_name=model_card_name,
            model_card_version=model_card_version,
            content=content,
            model_card_status=model_card_status,
            security_config=security_config,
            tags=tags,
        )

        self.model_cards[model_card_name].append(model_card)
        return model_card.arn

    def update_model_card(
        self, model_card_name: str, content: str, model_card_status: str
    ) -> str:
        if model_card_name not in self.model_cards:
            raise ResourceNotFound(f"Modelcard {model_card_name} does not exist.")

        datetime_now = utcnow()

        first_version = self.model_cards[model_card_name][0]
        creation_time = first_version.creation_time

        most_recent_version = self.model_cards[model_card_name][-1]
        next_version = most_recent_version.model_card_version + 1
        security_config = most_recent_version.security_config
        tags = most_recent_version.tags

        model_card = FakeModelCard(
            account_id=self.account_id,
            region_name=self.region_name,
            model_card_name=model_card_name,
            model_card_version=next_version,
            security_config=security_config,
            content=content,
            model_card_status=model_card_status,
            tags=tags,
            creation_time=creation_time,
            last_modified_time=datetime_now,
        )

        self.model_cards[model_card_name].append(model_card)
        return model_card.arn

    @paginate(pagination_model=PAGINATION_MODEL)
    def list_model_cards(
        self,
        creation_time_after: datetime | None,
        creation_time_before: datetime | None,
        name_contains: str | None,
        model_card_status: str | None,
        sort_by: str | None,
        sort_order: str | None,
    ) -> list[FakeModelCard]:
        model_cards = self.model_cards

        return filter_model_cards(
            model_cards,
            creation_time_after,
            creation_time_before,
            name_contains,
            model_card_status,
            sort_by,
            sort_order,
        )

    @paginate(pagination_model=PAGINATION_MODEL)
    def list_model_card_versions(
        self,
        creation_time_after: datetime | None,
        creation_time_before: datetime | None,
        model_card_name: str,
        model_card_status: str | None,
        sort_by: str | None,
        sort_order: str | None,
    ) -> list[FakeModelCard]:
        if model_card_name not in self.model_cards:
            raise ResourceNotFound(f"Modelcard {model_card_name} does not exist")

        versions = self.model_cards[model_card_name]
        if creation_time_after:
            versions = [
                v for v in versions if v.last_modified_time > creation_time_after
            ]
        if creation_time_before:
            versions = [
                v for v in versions if v.last_modified_time < creation_time_before
            ]
        if model_card_status:
            versions = [v for v in versions if v.model_card_status == model_card_status]

        reverse = sort_order == "Descending"

        return sorted(versions, key=lambda x: x.model_card_version, reverse=reverse)

    def describe_model_card(
        self, model_card_name: str, model_card_version: int
    ) -> dict[str, Any]:
        if model_card_name not in self.model_cards:
            raise ResourceNotFound(f"Modelcard {model_card_name} does not exist")

        versions = self.model_cards[model_card_name]
        if model_card_version:
            filtered = [
                v for v in versions if v.model_card_version == model_card_version
            ]
            if filtered:
                version = filtered[0]
                return version.describe()
            else:
                raise ResourceNotFound(
                    f"Modelcard with name {model_card_name} and version: {model_card_version} does not exist"
                )
        return versions[-1].describe()

    def delete_model_card(self, model_card_name: str) -> None:
        if model_card_name not in self.model_cards:
            raise ResourceNotFound(f"Modelcard {model_card_name} does not exist")

        del self.model_cards[model_card_name]

    def create_data_quality_job_definition(
        self,
        account_id: str,
        job_definition_name: str,
        tags: list[dict[str, str]] | None = None,
        role_arn: str = "",
        job_resources: dict[str, Any] | None = None,
        stopping_condition: dict[str, Any] | None = None,
        environment: dict[str, str] | None = None,
        network_config: dict[str, Any] | None = None,
        data_quality_baseline_config: dict[str, Any] | None = None,
        data_quality_app_specification: dict[str, Any] | None = None,
        data_quality_job_input: dict[str, Any] | None = None,
        data_quality_job_output_config: dict[str, Any] | None = None,
    ) -> dict[str, str]:
        job_definition = FakeDataQualityJobDefinition(
            account_id=account_id,
            region_name=self.region_name,
            job_definition_name=job_definition_name,
            tags=tags or [],
            role_arn=role_arn,
            job_resources=job_resources,
            stopping_condition=stopping_condition,
            environment=environment,
            network_config=network_config,
            data_quality_baseline_config=data_quality_baseline_config,
            data_quality_app_specification=data_quality_app_specification,
            data_quality_job_input=data_quality_job_input,
            data_quality_job_output_config=data_quality_job_output_config,
        )
        self.data_quality_job_definitions[job_definition_name] = job_definition
        return job_definition.response_create

    @paginate(pagination_model=PAGINATION_MODEL)
    def list_data_quality_job_definitions(self) -> list[dict[str, str]]:
        return [
            job.summary_object for job in self.data_quality_job_definitions.values()
        ]

    def describe_data_quality_job_definition(
        self, job_definition_name: str
    ) -> dict[str, Any]:
        job_definition = self.data_quality_job_definitions.get(job_definition_name)
        if job_definition is None:
            raise ResourceNotFound(f"Job definition {job_definition_name} not found")
        return job_definition.response_object

    def delete_data_quality_job_definition(self, job_definition_name: str) -> None:
        if job_definition_name in self.data_quality_job_definitions:
            del self.data_quality_job_definitions[job_definition_name]
        else:
            raise ResourceNotFound(f"Job definition {job_definition_name} not found")


class FakeDataQualityJobDefinition(BaseObject):
    def __init__(
        self,
        account_id: str,
        region_name: str,
        job_definition_name: str,
        tags: list[dict[str, str]] | None = None,
        role_arn: str = "",
        job_resources: dict[str, Any] | None = None,
        stopping_condition: dict[str, Any] | None = None,
        environment: dict[str, str] | None = None,
        network_config: dict[str, Any] | None = None,
        data_quality_baseline_config: dict[str, Any] | None = None,
        data_quality_app_specification: dict[str, Any] | None = None,
        data_quality_job_input: dict[str, Any] | None = None,
        data_quality_job_output_config: dict[str, Any] | None = None,
    ):
        self.job_definition_name = job_definition_name
        self.arn = FakeDataQualityJobDefinition.arn_formatter(
            job_definition_name, account_id, region_name
        )
        self.tags = tags or []
        self.role_arn = role_arn
        self.job_resources = job_resources or {}
        self.stopping_condition = stopping_condition or {}
        self.environment = environment or {}
        self.network_config = network_config or {}
        self.data_quality_baseline_config = data_quality_baseline_config or {}
        self.data_quality_app_specification = data_quality_app_specification or {}
        self.data_quality_job_input = data_quality_job_input or {}
        self.data_quality_job_output_config = data_quality_job_output_config or {}
        self.creation_time = self.last_modified_time = utcnow()

    @property
    def response_object(self) -> dict[str, str]:
        response_object = self.gen_response_object()
        response = {
            k: v for k, v in response_object.items() if v is not None and v != [None]
        }
        response["JobDefinitionArn"] = response.pop("Arn")
        return response

    @property
    def response_create(self) -> dict[str, str]:
        return {"JobDefinitionArn": self.arn}

    @staticmethod
    def arn_formatter(name: str, account_id: str, region: str) -> str:
        return arn_formatter("data-quality-job-definition", name, account_id, region)

    @property
    def summary_object(self) -> dict[str, Any]:
        return {
            "MonitoringJobDefinitionName": self.job_definition_name,
            "MonitoringJobDefinitionArn": self.arn,
            "CreationTime": self.creation_time,
            "EndpointName": "EndpointName",
        }


class FakeExperiment(BaseObject):
    def __init__(
        self,
        account_id: str,
        region_name: str,
        experiment_name: str,
        tags: list[dict[str, str]],
    ):
        self.experiment_name = experiment_name
        self.arn = arn_formatter("experiment", experiment_name, account_id, region_name)
        self.tags = tags
        self.creation_time = self.last_modified_time = utcnow()

    @property
    def response_object(self) -> dict[str, Any]:  # type: ignore[misc]
        response_object = self.gen_response_object()
        return {
            k: v for k, v in response_object.items() if v is not None and v != [None]
        }

    @property
    def response_create(self) -> dict[str, str]:
        return {"ExperimentArn": self.arn}


class FakeTrial(BaseObject):
    def __init__(
        self,
        account_id: str,
        region_name: str,
        trial_name: str,
        experiment_name: str,
        tags: list[dict[str, str]],
        trial_components: list[str],
    ):
        self.trial_name = trial_name
        self.arn = FakeTrial.arn_formatter(trial_name, account_id, region_name)
        self.tags = tags
        self.trial_components = trial_components
        self.experiment_name = experiment_name
        self.creation_time = self.last_modified_time = utcnow()

    @property
    def response_object(self) -> dict[str, Any]:  # type: ignore[misc]
        response_object = self.gen_response_object()
        response = {
            k: v for k, v in response_object.items() if v is not None and v != [None]
        }
        response["TrialArn"] = response.pop("Arn")

        return response

    @property
    def response_create(self) -> dict[str, str]:
        return {"TrialArn": self.arn}

    @staticmethod
    def arn_formatter(name: str, account_id: str, region: str) -> str:
        return arn_formatter("experiment-trial", name, account_id, region)


class FakeTrialComponent(BaseObject):
    def __init__(
        self,
        account_id: str,
        region_name: str,
        trial_component_name: str,
        display_name: str | None,
        start_time: datetime | None,
        end_time: datetime | None,
        parameters: dict[str, dict[str, str | float]] | None,
        input_artifacts: dict[str, dict[str, str]] | None,
        output_artifacts: dict[str, dict[str, str]] | None,
        metadata_properties: dict[str, str] | None,
        status: dict[str, str] | None,
        trial_name: str | None,
        tags: list[dict[str, str]],
    ):
        self.trial_component_name = trial_component_name
        self.display_name = (
            display_name if display_name is not None else trial_component_name
        )
        self.arn = FakeTrialComponent.arn_formatter(
            trial_component_name, account_id, region_name
        )
        self.status = status
        self.tags = tags
        self.trial_name = trial_name
        self.start_time = start_time
        self.end_time = end_time
        now = utcnow()
        self.creation_time = self.last_modified_time = now
        self.created_by: dict[str, dict[str, str] | str] = {}
        self.last_modified_by: dict[str, dict[str, str] | str] = {}
        self.parameters = parameters if parameters is not None else {}
        self.input_artifacts = input_artifacts if input_artifacts is not None else {}
        self.output_artifacts = output_artifacts if output_artifacts is not None else {}
        self.metadata_properties = metadata_properties
        self.metrics: dict[str, dict[str, str | int | METRIC_STEP_TYPE]] = {}
        self.sources: list[dict[str, str]] = []

    @property
    def response_object(self) -> dict[str, Any]:  # type: ignore[misc]
        response_object = self.gen_response_object()
        response_object["Metrics"] = self.gen_metrics_response_object()
        response = {
            k: v for k, v in response_object.items() if v is not None and v != [None]
        }
        response["TrialComponentArn"] = response.pop("Arn")

        return response

    def gen_metrics_response_object(
        self,
    ) -> list[dict[str, str | int | float | datetime]]:
        metrics_names = self.metrics.keys()
        metrics_response_objects = []
        for metrics_name in metrics_names:
            metrics_steps: METRIC_STEP_TYPE = cast(
                METRIC_STEP_TYPE, self.metrics[metrics_name]["Values"]
            )
            max_step = max(list(metrics_steps.keys()))
            metrics_steps_values: list[float] = [
                cast(float, metric["Value"]) for metric in list(metrics_steps.values())
            ]
            count = len(metrics_steps_values)
            mean = sum(metrics_steps_values) / count
            std = (
                sum((value - mean) ** 2 for value in metrics_steps_values) / count
            ) ** 0.5
            timestamp_int: int = cast(int, self.metrics[metrics_name]["Timestamp"])
            metrics_response_object = {
                "MetricName": metrics_name,
                "SourceArn": self.arn,
                "TimeStamp": datetime.fromtimestamp(timestamp_int, tz=timezone.utc),
                "Max": max(metrics_steps_values),
                "Min": min(metrics_steps_values),
                "Last": metrics_steps[max_step]["Value"],
                "Count": count,
                "Avg": mean,
                "StdDev": std,
            }
            metrics_response_objects.append(metrics_response_object)
        return metrics_response_objects

    @property
    def response_create(self) -> dict[str, str]:
        return {"TrialComponentArn": self.arn}

    @staticmethod
    def arn_formatter(
        trial_component_name: str, account_id: str, region_name: str
    ) -> str:
        return arn_formatter(
            "experiment-trial-component", trial_component_name, account_id, region_name
        )


class FakeModelBiasJobDefinition(BaseObject):
    def __init__(
        self,
        account_id: str,
        region_name: str,
        job_definition_name: str,
        tags: list[dict[str, str]] | None = None,
        role_arn: str = "",
        job_resources: dict[str, Any] | None = None,
        stopping_condition: dict[str, Any] | None = None,
        environment: dict[str, str] | None = None,
        network_config: dict[str, Any] | None = None,
        model_bias_baseline_config: dict[str, Any] | None = None,
        model_bias_app_specification: dict[str, Any] | None = None,
        model_bias_job_input: dict[str, Any] | None = None,
        model_bias_job_output_config: dict[str, Any] | None = None,
    ):
        self.job_definition_name = job_definition_name
        self.arn = FakeModelBiasJobDefinition.arn_formatter(
            job_definition_name, account_id, region_name
        )
        self.tags = tags or []
        self.role_arn = role_arn
        self.job_resources = job_resources or {}
        self.stopping_condition = stopping_condition or {}
        self.environment = environment or {}
        self.network_config = network_config or {}
        self.model_bias_baseline_config = model_bias_baseline_config or {}
        self.model_bias_app_specification = model_bias_app_specification or {}
        self.model_bias_job_input = model_bias_job_input or {}
        self.model_bias_job_output_config = model_bias_job_output_config or {}
        self.creation_time = self.last_modified_time = utcnow()

    @property
    def response_object(self) -> dict[str, str]:
        response_object = self.gen_response_object()
        response = {
            k: v for k, v in response_object.items() if v is not None and v != [None]
        }
        response["JobDefinitionArn"] = response.pop("Arn")
        return response

    @property
    def response_create(self) -> dict[str, str]:
        return {"JobDefinitionArn": self.arn}

    @staticmethod
    def arn_formatter(name: str, account_id: str, region: str) -> str:
        return f"arn:{get_partition(region)}:sagemaker:{region}:{account_id}:model-bias-job-definition/{name}"

    @property
    def summary_object(self) -> dict[str, str]:
        return {
            "MonitoringJobDefinitionName": self.job_definition_name,
            "MonitoringJobDefinitionArn": self.arn,
            "CreationTime": self.creation_time,
            "EndpointName": self.model_bias_job_input.get("EndpointInput", {}).get(
                "EndpointName", "EndpointName"
            ),
        }


sagemaker_backends = BackendDict(SageMakerModelBackend, "sagemaker")
