Repository: getmoto/moto
Pull request #7922 (2024-08-02 00:18:14)

Feature: SageMaker Model Cards
Adds support for SageMaker Model Card create, list, describe, delete

sjcahill: Hey, just curious what the recommended way to resolve conflicts is?

I sync'ed master on my fork, pulled the changes down and then rebased my feature branch to master. Should I just force-push to my remote feature? Or should I revert this and merge master into my feature branch
bblommers: Hi @sjcahill, I usually just force push. As long as there's only one person working on that branch, that shouldn't be a problem.

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index f8c172102..29333c20d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -3,7 +3,7 @@ Moto Changelog
 
 5.0.12
 -----
-Docker Digest for 5.0.12: <autopopulateddigest>
+Docker Digest for 5.0.12: _sha256:e1bde8f908f2fdf0878c8e4398561badd016b51b4d9fd8dcb9f4daef936a4427_
 
     General:
         * The MotoProxy can now be run on Windows
diff --git a/moto/__init__.py b/moto/__init__.py
index 726f3bbe8..f4b8ab0fa 100644
--- a/moto/__init__.py
+++ b/moto/__init__.py
@@ -1,4 +1,4 @@
 from moto.core.decorator import mock_aws as mock_aws
 
 __title__ = "moto"
-__version__ = "5.0.12"
+__version__ = "5.0.13.dev"
diff --git a/moto/rds/models.py b/moto/rds/models.py
index 4e21129a9..af34de951 100644
--- a/moto/rds/models.py
+++ b/moto/rds/models.py
@@ -1127,18 +1127,6 @@ class DBInstance(CloudFormationModel, RDSBaseModel):
             database = rds_backend.create_db_instance(db_kwargs)
         return database
 
-    def get_tags(self) -> List[Dict[str, str]]:
-        return self.tags
-
-    def add_tags(self, tags: List[Dict[str, str]]) -> List[Dict[str, str]]:
-        new_keys = [tag_set["Key"] for tag_set in tags]
-        self.tags = [tag_set for tag_set in self.tags if tag_set["Key"] not in new_keys]
-        self.tags.extend(tags)
-        return self.tags
-
-    def remove_tags(self, tag_keys: List[str]) -> None:
-        self.tags = [tag_set for tag_set in self.tags if tag_set["Key"] not in tag_keys]
-
     def delete(self, account_id: str, region_name: str) -> None:
         backend = rds_backends[account_id][region_name]
         backend.delete_db_instance(self.db_instance_identifier)  # type: ignore[arg-type]
@@ -1661,9 +1649,21 @@ class RDSBackend(BaseBackend):
         self.subnet_groups: Dict[str, DBSubnetGroup] = {}
         self._db_cluster_options: Optional[List[Dict[str, Any]]] = None
         self.db_proxies: Dict[str, DBProxy] = OrderedDict()
-
-    def reset(self) -> None:
-        super().reset()
+        self.resource_map = {
+            DBCluster: self.clusters,
+            DBClusterParameterGroup: self.db_cluster_parameter_groups,
+            DBClusterSnapshot: self.cluster_snapshots,
+            DBInstance: self.databases,
+            DBParameterGroup: self.db_parameter_groups,
+            DBProxy: self.db_proxies,
+            DBSecurityGroup: self.security_groups,
+            DBSnapshot: self.database_snapshots,
+            DBSubnetGroup: self.subnet_groups,
+            EventSubscription: self.event_subscriptions,
+            ExportTask: self.export_tasks,
+            GlobalCluster: self.global_clusters,
+            OptionGroup: self.option_groups,
+        }
 
     @lru_cache()
     def db_cluster_options(self, engine) -> List[Dict[str, Any]]:  # type: ignore
@@ -2116,24 +2116,6 @@ class RDSBackend(BaseBackend):
         self, option_group_kwargs: Dict[str, Any]
     ) -> List["OptionGroup"]:
         option_group_list = []
-
-        if option_group_kwargs["marker"]:
-            marker = option_group_kwargs["marker"]
-        else:
-            marker = 0
-        if option_group_kwargs["max_records"]:
-            if (
-                option_group_kwargs["max_records"] < 20
-                or option_group_kwargs["max_records"] > 100
-            ):
-                raise RDSClientError(
-                    "InvalidParameterValue",
-                    "Invalid value for max records. Must be between 20 and 100",
-                )
-            max_records = option_group_kwargs["max_records"]
-        else:
-            max_records = 100
-
         for option_group in self.option_groups.values():
             if (
                 option_group_kwargs["name"]
@@ -2155,7 +2137,7 @@ class RDSBackend(BaseBackend):
                 option_group_list.append(option_group)
         if not len(option_group_list):
             raise OptionGroupNotFoundFaultError(option_group_kwargs["name"])
-        return option_group_list[marker : max_records + marker]
+        return option_group_list
 
     @staticmethod
     def describe_option_group_options(
@@ -2247,24 +2229,6 @@ class RDSBackend(BaseBackend):
         self, db_parameter_group_kwargs: Dict[str, Any]
     ) -> List["DBParameterGroup"]:
         db_parameter_group_list = []
-
-        if db_parameter_group_kwargs.get("marker"):
-            marker = db_parameter_group_kwargs["marker"]
-        else:
-            marker = 0
-        if db_parameter_group_kwargs.get("max_records"):
-            if (
-                db_parameter_group_kwargs["max_records"] < 20
-                or db_parameter_group_kwargs["max_records"] > 100
-            ):
-                raise RDSClientError(
-                    "InvalidParameterValue",
-                    "Invalid value for max records. Must be between 20 and 100",
-                )
-            max_records = db_parameter_group_kwargs["max_records"]
-        else:
-            max_records = 100
-
         for db_parameter_group in self.db_parameter_groups.values():
             if not db_parameter_group_kwargs.get(
                 "name"
@@ -2272,8 +2236,7 @@ class RDSBackend(BaseBackend):
                 db_parameter_group_list.append(db_parameter_group)
             else:
                 continue
-
-        return db_parameter_group_list[marker : max_records + marker]
+        return db_parameter_group_list
 
     def modify_db_parameter_group(
         self,
@@ -2582,92 +2545,33 @@ class RDSBackend(BaseBackend):
                 raise SubscriptionNotFoundError(subscription_name)
         return self.event_subscriptions.values()
 
+    def _find_resource(self, resource_type: str, resource_name: str) -> Any:
+        for resource_class, resources in self.resource_map.items():
+            if resource_type == getattr(resource_class, "resource_type", ""):
+                if resource_name in resources:  # type: ignore
+                    return resources[resource_name]  # type: ignore
+
     def list_tags_for_resource(self, arn: str) -> List[Dict[str, str]]:
         if self.arn_regex.match(arn):
             arn_breakdown = arn.split(":")
             resource_type = arn_breakdown[len(arn_breakdown) - 2]
             resource_name = arn_breakdown[len(arn_breakdown) - 1]
-            if resource_type == "db":  # Database
-                if resource_name in self.databases:
-                    return self.databases[resource_name].get_tags()
-            elif resource_type == "cluster":  # Cluster
-                if resource_name in self.clusters:
-                    return self.clusters[resource_name].get_tags()
-            elif resource_type == "es":  # Event Subscription
-                if resource_name in self.event_subscriptions:
-                    return self.event_subscriptions[resource_name].get_tags()
-            elif resource_type == "og":  # Option Group
-                if resource_name in self.option_groups:
-                    return self.option_groups[resource_name].get_tags()
-            elif resource_type == "pg":  # Parameter Group
-                if resource_name in self.db_parameter_groups:
-                    return self.db_parameter_groups[resource_name].get_tags()
-            elif resource_type == "ri":  # Reserved DB instance
-                # TODO: Complete call to tags on resource type Reserved DB
-                # instance
-                return []
-            elif resource_type == "secgrp":  # DB security group
-                if resource_name in self.security_groups:
-                    return self.security_groups[resource_name].get_tags()
-            elif resource_type == "snapshot":  # DB Snapshot
-                if resource_name in self.database_snapshots:
-                    return self.database_snapshots[resource_name].get_tags()
-            elif resource_type == "cluster-snapshot":  # DB Cluster Snapshot
-                if resource_name in self.cluster_snapshots:
-                    return self.cluster_snapshots[resource_name].get_tags()
-            elif resource_type == "subgrp":  # DB subnet group
-                if resource_name in self.subnet_groups:
-                    return self.subnet_groups[resource_name].get_tags()
-            elif resource_type == "db-proxy":  # DB Proxy
-                if resource_name in self.db_proxies:
-                    return self.db_proxies[resource_name].get_tags()
-        else:
-            raise RDSClientError(
-                "InvalidParameterValue", f"Invalid resource name: {arn}"
-            )
-        return []
+            resource = self._find_resource(resource_type, resource_name)
+            if resource:
+                return resource.get_tags()
+            return []
+        raise RDSClientError("InvalidParameterValue", f"Invalid resource name: {arn}")
 
     def remove_tags_from_resource(self, arn: str, tag_keys: List[str]) -> None:
         if self.arn_regex.match(arn):
             arn_breakdown = arn.split(":")
             resource_type = arn_breakdown[len(arn_breakdown) - 2]
             resource_name = arn_breakdown[len(arn_breakdown) - 1]
-            if resource_type == "db":  # Database
-                if resource_name in self.databases:
-                    self.databases[resource_name].remove_tags(tag_keys)
-            elif resource_type == "es":  # Event Subscription
-                if resource_name in self.event_subscriptions:
-                    self.event_subscriptions[resource_name].remove_tags(tag_keys)
-            elif resource_type == "og":  # Option Group
-                if resource_name in self.option_groups:
-                    self.option_groups[resource_name].remove_tags(tag_keys)
-            elif resource_type == "pg":  # Parameter Group
-                if resource_name in self.db_parameter_groups:
-                    self.db_parameter_groups[resource_name].remove_tags(tag_keys)
-            elif resource_type == "ri":  # Reserved DB instance
-                return None
-            elif resource_type == "secgrp":  # DB security group
-                if resource_name in self.security_groups:
-                    self.security_groups[resource_name].remove_tags(tag_keys)
-            elif resource_type == "snapshot":  # DB Snapshot
-                if resource_name in self.database_snapshots:
-                    self.database_snapshots[resource_name].remove_tags(tag_keys)
-            elif resource_type == "cluster":
-                if resource_name in self.clusters:
-                    self.clusters[resource_name].remove_tags(tag_keys)
-            elif resource_type == "cluster-snapshot":  # DB Cluster Snapshot
-                if resource_name in self.cluster_snapshots:
-                    self.cluster_snapshots[resource_name].remove_tags(tag_keys)
-            elif resource_type == "subgrp":  # DB subnet group
-                if resource_name in self.subnet_groups:
-                    self.subnet_groups[resource_name].remove_tags(tag_keys)
-            elif resource_type == "db-proxy":  # DB Proxy
-                if resource_name in self.db_proxies:
-                    self.db_proxies[resource_name].remove_tags(tag_keys)
-        else:
-            raise RDSClientError(
-                "InvalidParameterValue", f"Invalid resource name: {arn}"
-            )
+            resource = self._find_resource(resource_type, resource_name)
+            if resource:
+                resource.remove_tags(tag_keys)
+            return
+        raise RDSClientError("InvalidParameterValue", f"Invalid resource name: {arn}")
 
     def add_tags_to_resource(  # type: ignore[return]
         self, arn: str, tags: List[Dict[str, str]]
@@ -2676,42 +2580,11 @@ class RDSBackend(BaseBackend):
             arn_breakdown = arn.split(":")
             resource_type = arn_breakdown[-2]
             resource_name = arn_breakdown[-1]
-            if resource_type == "db":  # Database
-                if resource_name in self.databases:
-                    return self.databases[resource_name].add_tags(tags)
-            elif resource_type == "es":  # Event Subscription
-                if resource_name in self.event_subscriptions:
-                    return self.event_subscriptions[resource_name].add_tags(tags)
-            elif resource_type == "og":  # Option Group
-                if resource_name in self.option_groups:
-                    return self.option_groups[resource_name].add_tags(tags)
-            elif resource_type == "pg":  # Parameter Group
-                if resource_name in self.db_parameter_groups:
-                    return self.db_parameter_groups[resource_name].add_tags(tags)
-            elif resource_type == "ri":  # Reserved DB instance
-                return []
-            elif resource_type == "secgrp":  # DB security group
-                if resource_name in self.security_groups:
-                    return self.security_groups[resource_name].add_tags(tags)
-            elif resource_type == "snapshot":  # DB Snapshot
-                if resource_name in self.database_snapshots:
-                    return self.database_snapshots[resource_name].add_tags(tags)
-            elif resource_type == "cluster":
-                if resource_name in self.clusters:
-                    return self.clusters[resource_name].add_tags(tags)
-            elif resource_type == "cluster-snapshot":  # DB Cluster Snapshot
-                if resource_name in self.cluster_snapshots:
-                    return self.cluster_snapshots[resource_name].add_tags(tags)
-            elif resource_type == "subgrp":  # DB subnet group
-                if resource_name in self.subnet_groups:
-                    return self.subnet_groups[resource_name].add_tags(tags)
-            elif resource_type == "db-proxy":  # DB Proxy
-                if resource_name in self.db_proxies:
-                    return self.db_proxies[resource_name].add_tags(tags)
-        else:
-            raise RDSClientError(
-                "InvalidParameterValue", f"Invalid resource name: {arn}"
-            )
+            resource = self._find_resource(resource_type, resource_name)
+            if resource:
+                return resource.add_tags(tags)
+            return []
+        raise RDSClientError("InvalidParameterValue", f"Invalid resource name: {arn}")
 
     @staticmethod
     def _filter_resources(resources: Any, filters: Any, resource_class: Any) -> Any:  # type: ignore[misc]
diff --git a/moto/rds/responses.py b/moto/rds/responses.py
index b39bd61cd..632d2327e 100644
--- a/moto/rds/responses.py
+++ b/moto/rds/responses.py
@@ -1,5 +1,5 @@
 from collections import defaultdict
-from typing import Any, Dict, Iterable, List
+from typing import Any, Dict, Iterable, List, Optional, Tuple
 
 from moto.core.responses import BaseResponse
 from moto.ec2.models import ec2_backends
@@ -255,20 +255,7 @@ class RDSResponse(BaseResponse):
                 db_instance_identifier, filters=filter_dict
             )
         )
-        marker = self._get_param("Marker")
-        all_ids = [instance.db_instance_identifier for instance in all_instances]
-        if marker:
-            start = all_ids.index(marker) + 1
-        else:
-            start = 0
-        page_size = self._get_int_param(
-            "MaxRecords", 50
-        )  # the default is 100, but using 50 to make testing easier
-        instances_resp = all_instances[start : start + page_size]
-        next_marker = None
-        if len(all_instances) > start + page_size:
-            next_marker = instances_resp[-1].db_instance_identifier
-
+        instances_resp, next_marker = self._paginate(all_instances)
         template = self.response_template(DESCRIBE_DATABASES_TEMPLATE)
         return template.render(databases=instances_resp, marker=next_marker)
 
@@ -485,9 +472,8 @@ class RDSResponse(BaseResponse):
 
     def describe_option_groups(self) -> str:
         kwargs = self._get_option_group_kwargs()
-        kwargs["max_records"] = self._get_int_param("MaxRecords")
-        kwargs["marker"] = self._get_param("Marker")
         option_groups = self.backend.describe_option_groups(kwargs)
+        option_groups, _ = self._paginate(option_groups)
         template = self.response_template(DESCRIBE_OPTION_GROUP_TEMPLATE)
         return template.render(option_groups=option_groups)
 
@@ -542,9 +528,8 @@ class RDSResponse(BaseResponse):
 
     def describe_db_parameter_groups(self) -> str:
         kwargs = self._get_db_parameter_group_kwargs()
-        kwargs["max_records"] = self._get_int_param("MaxRecords")
-        kwargs["marker"] = self._get_param("Marker")
         db_parameter_groups = self.backend.describe_db_parameter_groups(kwargs)
+        db_parameter_groups, _ = self._paginate(db_parameter_groups)
         template = self.response_template(DESCRIBE_DB_PARAMETER_GROUPS_TEMPLATE)
         return template.render(db_parameter_groups=db_parameter_groups)
 
@@ -904,6 +889,29 @@ class RDSResponse(BaseResponse):
         template = self.response_template(CREATE_DB_PROXY_TEMPLATE)
         return template.render(dbproxy=db_proxy)
 
+    def _paginate(self, resources: List[Any]) -> Tuple[List[Any], Optional[str]]:
+        from moto.rds.exceptions import InvalidParameterValue
+
+        marker = self._get_param("Marker")
+        # Default was originally set to 50 instead of 100 for ease of testing.  Should fix.
+        page_size = self._get_int_param("MaxRecords", 50)
+        if page_size < 20 or page_size > 100:
+            msg = (
+                f"Invalid value {page_size} for MaxRecords. Must be between 20 and 100"
+            )
+            raise InvalidParameterValue(msg)
+        all_resources = list(resources)
+        all_ids = [resource.name for resource in all_resources]
+        if marker:
+            start = all_ids.index(marker) + 1
+        else:
+            start = 0
+        paginated_resources = all_resources[start : start + page_size]
+        next_marker = None
+        if len(all_resources) > start + page_size:
+            next_marker = paginated_resources[-1].name
+        return paginated_resources, next_marker
+
 
 CREATE_DATABASE_TEMPLATE = """<CreateDBInstanceResponse xmlns="http://rds.amazonaws.com/doc/2014-09-01/">
   <CreateDBInstanceResult>
diff --git a/moto/sagemaker/exceptions.py b/moto/sagemaker/exceptions.py
index c2286b9fe..64a5bd9c8 100644
--- a/moto/sagemaker/exceptions.py
+++ b/moto/sagemaker/exceptions.py
@@ -45,6 +45,10 @@ class ResourceInUseException(AWSError):
     TYPE = "ResourceInUse"
 
 
+class ConflictException(AWSError):
+    TYPE = "ConflictException"
+
+
 class ResourceNotFound(JsonRESTError):
     def __init__(self, message: str):
         super().__init__(__class__.__name__, message)  # type: ignore
diff --git a/moto/sagemaker/models.py b/moto/sagemaker/models.py
index d2e0afa1d..5b97304ed 100644
--- a/moto/sagemaker/models.py
+++ b/moto/sagemaker/models.py
@@ -3,8 +3,9 @@ import os
 import random
 import re
 import string
+from collections import defaultdict
 from datetime import datetime
-from typing import Any, Dict, Iterable, List, Optional, Union, cast
+from typing import Any, DefaultDict, Dict, Iterable, List, Optional, Union, cast
 
 from dateutil.tz import tzutc
 
@@ -16,6 +17,7 @@ from moto.utilities.utils import ARN_PARTITION_REGEX, get_partition
 
 from .exceptions import (
     AWSValidationException,
+    ConflictException,
     MissingModel,
     ResourceInUseException,
     ResourceNotFound,
@@ -23,6 +25,7 @@ from .exceptions import (
 )
 from .utils import (
     arn_formatter,
+    filter_model_cards,
     get_pipeline_execution_from_arn,
     get_pipeline_from_name,
     get_pipeline_name_from_execution_arn,
@@ -133,6 +136,18 @@ PAGINATION_MODEL = {
         "limit_default": 100,
         "unique_attribute": "arn",
     },
+    "list_model_cards": {
+        "input_token": "next_token",
+        "limit_key": "max_results",
+        "limit_default": 100,
+        "unique_attribute": "model_card_arn",
+    },
+    "list_model_card_versions": {
+        "input_token": "next_token",
+        "limit_key": "max_results",
+        "limit_default": 100,
+        "unique_attribute": "model_card_arn",
+    },
 }
 
 METRIC_INFO_TYPE = Dict[str, Union[str, int, float, datetime]]
@@ -1063,6 +1078,69 @@ class ModelPackageGroup(BaseObject):
         return {k: v for k, v in response_object.items() if k in response_values}
 
 
+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: Optional[Dict[str, str]] = None,
+        tags: Optional[List[Dict[str, Any]]] = None,
+        creation_time: Optional[str] = None,
+        last_modified_time: Optional[str] = None,
+    ) -> None:
+        datetime_now = str(datetime.now(tzutc()))
+        self.model_card_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.model_card_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.model_card_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.model_card_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,
@@ -2780,6 +2858,7 @@ class SageMakerModelBackend(BaseBackend):
         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] = {}
@@ -2929,6 +3008,7 @@ class SageMakerModelBackend(BaseBackend):
             "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:
@@ -2936,6 +3016,8 @@ class SageMakerModelBackend(BaseBackend):
         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]]:
@@ -5432,6 +5514,147 @@ class SageMakerModelBackend(BaseBackend):
             )
         del self.model_quality_job_definitions[job_definition_name]
 
+    def create_model_card(
+        self,
+        model_card_name: str,
+        security_config: Optional[Dict[str, str]],
+        content: str,
+        model_card_status: str,
+        tags: Optional[List[Dict[str, str]]],
+        model_card_version: Optional[int] = None,
+        creation_time: Optional[str] = None,
+        last_modified_time: Optional[str] = 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.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 = str(datetime.now(tzutc()))
+
+        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.model_card_arn
+
+    @paginate(pagination_model=PAGINATION_MODEL)
+    def list_model_cards(
+        self,
+        creation_time_after: Optional[datetime],
+        creation_time_before: Optional[datetime],
+        name_contains: Optional[str],
+        model_card_status: Optional[str],
+        sort_by: Optional[str],
+        sort_order: Optional[str],
+    ) -> 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: Optional[datetime],
+        creation_time_before: Optional[datetime],
+        model_card_name: str,
+        model_card_status: Optional[str],
+        sort_by: Optional[str],
+        sort_order: Optional[str],
+    ) -> 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 > str(creation_time_after)
+            ]
+        if creation_time_before:
+            versions = [
+                v for v in versions if v.last_modified_time < str(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]
+
 
 class FakeExperiment(BaseObject):
     def __init__(
diff --git a/moto/sagemaker/responses.py b/moto/sagemaker/responses.py
index 225d64761..df44a2ce6 100644
--- a/moto/sagemaker/responses.py
+++ b/moto/sagemaker/responses.py
@@ -1476,3 +1476,100 @@ class SageMakerResponse(BaseResponse):
             job_definition_name=job_definition_name,
         )
         return json.dumps(dict())
+
+    def create_model_card(self) -> str:
+        model_card_name = self._get_param("ModelCardName")
+        security_config = self._get_param("SecurityConfig")
+        content = self._get_param("Content")
+        model_card_status = self._get_param("ModelCardStatus")
+        tags = self._get_param("Tags")
+        model_card_arn = self.sagemaker_backend.create_model_card(
+            model_card_name=model_card_name,
+            security_config=security_config,
+            content=content,
+            model_card_status=model_card_status,
+            tags=tags,
+        )
+        return json.dumps(dict(ModelCardArn=model_card_arn))
+
+    def list_model_cards(self) -> str:
+        creation_time_after = self._get_param("CreationTimeAfter")
+        creation_time_before = self._get_param("CreationTimeBefore")
+        max_results = self._get_param("MaxResults")
+        name_contains = self._get_param("NameContains")
+        model_card_status = self._get_param("ModelCardStatus")
+        next_token = self._get_param("NextToken")
+        sort_by = self._get_param("SortBy")
+        sort_order = self._get_param("SortOrder")
+        model_cards, next_token = self.sagemaker_backend.list_model_cards(
+            creation_time_after=creation_time_after,
+            creation_time_before=creation_time_before,
+            max_results=max_results,
+            name_contains=name_contains,
+            model_card_status=model_card_status,
+            next_token=next_token,
+            sort_by=sort_by,
+            sort_order=sort_order,
+        )
+        model_card_summaries = [model_card.summary() for model_card in model_cards]
+        return json.dumps(
+            dict(ModelCardSummaries=model_card_summaries, NextToken=next_token)
+        )
+
+    def list_model_card_versions(self) -> str:
+        creation_time_after = self._get_param("CreationTimeAfter")
+        creation_time_before = self._get_param("CreationTimeBefore")
+        max_results = self._get_param("MaxResults")
+        model_card_name = self._get_param("ModelCardName")
+        model_card_status = self._get_param("ModelCardStatus")
+        next_token = self._get_param("NextToken")
+        sort_by = self._get_param("SortBy")
+        sort_order = self._get_param("SortOrder")
+        model_card_versions, next_token = (
+            self.sagemaker_backend.list_model_card_versions(
+                creation_time_after=creation_time_after,
+                creation_time_before=creation_time_before,
+                max_results=max_results,
+                model_card_name=model_card_name,
+                model_card_status=model_card_status,
+                next_token=next_token,
+                sort_by=sort_by,
+                sort_order=sort_order,
+            )
+        )
+        model_card_version_summaries = [
+            mcv.version_summary() for mcv in model_card_versions
+        ]
+        return json.dumps(
+            dict(
+                ModelCardVersionSummaryList=model_card_version_summaries,
+                NextToken=next_token,
+            )
+        )
+
+    def update_model_card(self) -> str:
+        model_card_name = self._get_param("ModelCardName")
+        content = self._get_param("Content")
+        model_card_status = self._get_param("ModelCardStatus")
+        model_card_arn = self.sagemaker_backend.update_model_card(
+            model_card_name=model_card_name,
+            content=content,
+            model_card_status=model_card_status,
+        )
+        return json.dumps(dict(ModelCardArn=model_card_arn))
+
+    def describe_model_card(self) -> str:
+        model_card_name = self._get_param("ModelCardName")
+        model_card_version = self._get_param("ModelCardVersion")
+        model_card_description = self.sagemaker_backend.describe_model_card(
+            model_card_name=model_card_name,
+            model_card_version=model_card_version,
+        )
+        return json.dumps(model_card_description)
+
+    def delete_model_card(self) -> str:
+        model_card_name = self._get_param("ModelCardName")
+        self.sagemaker_backend.delete_model_card(
+            model_card_name=model_card_name,
+        )
+        return json.dumps(dict())
diff --git a/moto/sagemaker/utils.py b/moto/sagemaker/utils.py
index ed8018672..9c038ad11 100644
--- a/moto/sagemaker/utils.py
+++ b/moto/sagemaker/utils.py
@@ -1,6 +1,9 @@
 import json
 import typing
-from typing import Any, Dict
+from datetime import datetime
+from typing import Any, DefaultDict, Dict, List, Optional
+
+from dateutil.tz import tzutc
 
 from moto.s3.models import s3_backends
 from moto.utilities.utils import get_partition
@@ -8,7 +11,7 @@ from moto.utilities.utils import get_partition
 from .exceptions import ValidationError
 
 if typing.TYPE_CHECKING:
-    from .models import FakePipeline, FakePipelineExecution
+    from .models import FakeModelCard, FakePipeline, FakePipelineExecution
 
 
 def get_pipeline_from_name(
@@ -64,3 +67,68 @@ def validate_model_approval_status(model_approval_status: typing.Optional[str])
             f"Value '{model_approval_status}' at 'modelApprovalStatus' failed to satisfy constraint: "
             "Member must satisfy enum value set: [PendingManualApproval, Approved, Rejected]"
         )
+
+
+def filter_model_cards(
+    model_cards: DefaultDict[str, List["FakeModelCard"]],
+    creation_time_after: Optional[datetime],
+    creation_time_before: Optional[datetime],
+    name_contains: Optional[str],
+    model_card_status: Optional[str],
+    sort_by: Optional[str],
+    sort_order: Optional[str],
+) -> List["FakeModelCard"]:
+    reverse = sort_order == "Descending"
+
+    if name_contains:
+        lowercase_name = name_contains.lower()
+        filtered_cards = {
+            k: v for k, v in model_cards.items() if lowercase_name in k.lower()
+        }
+    else:
+        filtered_cards = {k: v for k, v in model_cards.items()}
+
+    result: List[FakeModelCard] = []
+    for _, versions in filtered_cards.items():
+        filtered_versions = versions
+
+        if creation_time_after:
+            if isinstance(creation_time_after, int):
+                creation_time_after = datetime.fromtimestamp(
+                    creation_time_after, tz=tzutc()
+                )
+            filtered_versions = [
+                v
+                for v in filtered_versions
+                if v.last_modified_time > str(creation_time_after)
+            ]
+
+        if creation_time_before:
+            if isinstance(creation_time_before, int):
+                creation_time_before = datetime.fromtimestamp(
+                    creation_time_before, tz=tzutc()
+                )
+            filtered_versions = [
+                v
+                for v in filtered_versions
+                if v.last_modified_time < str(creation_time_before)
+            ]
+
+        if model_card_status:
+            filtered_versions = [
+                v for v in filtered_versions if v.model_card_status == model_card_status
+            ]
+
+        if filtered_versions:
+            latest_version = max(filtered_versions, key=lambda v: v.last_modified_time)
+            result.append(latest_version)
+
+    if not result:
+        return []
+
+    def sort_key(x: "FakeModelCard") -> str:
+        if sort_by == "Name":
+            return x.model_card_name
+        return x.creation_time
+
+    return sorted(result, key=sort_key, reverse=reverse)
diff --git a/setup.cfg b/setup.cfg
index 7edb9b5a5..28b41da35 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -1,6 +1,6 @@
 [metadata]
 name = moto
-version = 5.0.12
+version = 5.0.13.dev
 long_description = file:README.md
 long_description_content_type = text/markdown
 author = Steve Pulec


Test patch:
diff --git a/tests/test_sagemaker/test_sagemaker_model_cards.py b/tests/test_sagemaker/test_sagemaker_model_cards.py
new file mode 100644
index 000000000..655747eb2
--- /dev/null
+++ b/tests/test_sagemaker/test_sagemaker_model_cards.py
@@ -0,0 +1,394 @@
+"""Unit tests for sagemaker-supported APIs."""
+
+import time
+from datetime import datetime
+
+import boto3
+import pytest
+from botocore.exceptions import ClientError
+from dateutil.tz import tzutc
+
+from moto import mock_aws
+
+# See our Development Tips on writing tests for hints on how to write good tests:
+# http://docs.getmoto.org/en/latest/docs/contributing/development_tips/tests.html
+
+ACCOUNT_ID = "123456789012"
+
+
+@mock_aws
+def test_create_model_card():
+    client = boto3.client("sagemaker", region_name="us-east-1")
+    resp = client.create_model_card(
+        ModelCardName="my-first-model-card",
+        ModelCardStatus="Draft",
+        Content='{"model_overview": {"model_description": "my first model"}}',
+    )
+
+    assert (
+        resp["ModelCardArn"]
+        == f"arn:aws:sagemaker:us-east-1:{ACCOUNT_ID}:model-card/my-first-model-card"
+    )
+
+    with pytest.raises(ClientError) as e:
+        resp = client.create_model_card(
+            ModelCardName="my-first-model-card",
+            ModelCardStatus="Draft",
+            Content='{"model_overview": {"model_description": "my first model"}}',
+        )
+    assert e.value.response["Error"]["Code"] == "ConflictException"
+    assert (
+        e.value.response["Error"]["Message"]
+        == "Modelcard my-first-model-card already exists"
+    )
+
+
+@mock_aws
+def test_update_model_card():
+    client = boto3.client("sagemaker", region_name="us-east-1")
+    client.create_model_card(
+        ModelCardName="my-first-model-card",
+        ModelCardStatus="Draft",
+        Content='{"model_overview": {"model_description": "my first model"}}',
+    )
+    client.update_model_card(
+        ModelCardName="my-first-model-card",
+        ModelCardStatus="Draft",
+        Content='{"model_overview": {"model_description": "my first model ."}}',
+    )
+
+    resp = client.list_model_card_versions(ModelCardName="my-first-model-card").get(
+        "ModelCardVersionSummaryList"
+    )
+    assert (len(resp)) == 2
+
+    with pytest.raises(ClientError) as e:
+        client.update_model_card(
+            ModelCardName="my-second-model-card",
+            ModelCardStatus="Draft",
+            Content='{"model_overview": {"model_description": "my first model ."}}',
+        )
+    assert e.value.response["Error"]["Code"] == "ResourceNotFound"
+    assert (
+        e.value.response["Error"]["Message"]
+        == "Modelcard my-second-model-card does not exist."
+    )
+
+
+@mock_aws
+def test_model_card_tags():
+    client = boto3.client("sagemaker", region_name="us-east-1")
+    client.create_model_card(
+        ModelCardName="my-first-model-card",
+        ModelCardStatus="Draft",
+        Content='{"model_overview": {"model_description": "my first model"}}',
+        Tags=[{"Key": "Mytag", "Value": "MyValue"}],
+    )
+
+    model_card_arn = (
+        f"arn:aws:sagemaker:us-east-1:{ACCOUNT_ID}:model-card/my-first-model-card"
+    )
+    tags = client.list_tags(ResourceArn=model_card_arn).get("Tags")
+    assert [{"Key": "Mytag", "Value": "MyValue"}] == tags
+
+    client.update_model_card(
+        ModelCardName="my-first-model-card",
+        ModelCardStatus="Draft",
+        Content='{"model_overview": {"model_description": "my first model >:)"}}',
+    )
+
+    model_card_arn = (
+        f"arn:aws:sagemaker:us-east-1:{ACCOUNT_ID}:model-card/my-first-model-card"
+    )
+    tags = client.list_tags(ResourceArn=model_card_arn).get("Tags")
+    assert [{"Key": "Mytag", "Value": "MyValue"}] == tags
+
+    client.add_tags(
+        ResourceArn=model_card_arn, Tags=[{"Key": "Mytag2", "Value": "MyValue2"}]
+    )
+    tags = client.list_tags(ResourceArn=model_card_arn).get("Tags")
+    assert {"Key": "Mytag2", "Value": "MyValue2"} in tags
+
+    tags_to_delete = ["Mytag"]
+    client.delete_tags(ResourceArn=model_card_arn, TagKeys=tags_to_delete)
+    tags = client.list_tags(ResourceArn=model_card_arn).get("Tags")
+    assert {"Key": "Mytag", "Value": "MyValue"} not in tags
+
+
+@mock_aws
+def test_list_model_cards_basic():
+    client = boto3.client("sagemaker", region_name="us-east-1")
+
+    cards_to_create = ["first", "second", "third"]
+    for c in cards_to_create:
+        client.create_model_card(
+            ModelCardName=f"my-{c}-model-card",
+            ModelCardStatus="Draft",
+            Content='{"model_overview": {"model_description": f"my {c} model"}}',
+        )
+    resp = client.list_model_cards().get("ModelCardSummaries")
+    assert len(resp) == 3
+    for i, r in enumerate(resp):
+        assert (
+            r["ModelCardName"] == f"my-{cards_to_create[i]}-model-card"
+        ), "model card name didn't match expected"
+        assert (
+            r["ModelCardArn"]
+            == f"arn:aws:sagemaker:us-east-1:{ACCOUNT_ID}:model-card/my-{cards_to_create[i]}-model-card"
+        ), "model_card_arn didn't match expected"
+        assert (
+            r["ModelCardStatus"] == "Draft"
+        ), "model card status didn't match expected"
+
+
+@mock_aws
+def test_list_model_cards_advanced():
+    client = boto3.client("sagemaker", region_name="us-east-1")
+
+    cards_to_create = ["first", "second", "third"]
+    for c in cards_to_create:
+        client.create_model_card(
+            ModelCardName=f"my-{c}-model-card",
+            ModelCardStatus="Draft",
+            Content='{"model_overview": {"model_description": f"my {c} model"}}',
+        )
+    time.sleep(1)
+    datetime_now = datetime.now(tzutc())
+    client.create_model_card(
+        ModelCardName="my-fourth-model-card",
+        ModelCardStatus="Approved",
+        Content='{"model_overview": {"model_description": "my fourth model"}}',
+    )
+
+    resp = client.list_model_cards(NameContains="first").get("ModelCardSummaries")
+    assert len(resp) == 1
+    assert resp[0].get("ModelCardName") == "my-first-model-card"
+
+    resp = client.list_model_cards().get("ModelCardSummaries")
+    assert len(resp) == 4
+    assert resp[-1].get("ModelCardName") == "my-fourth-model-card"
+
+    # Ascending by default
+    resp = client.list_model_cards(SortOrder="Descending").get("ModelCardSummaries")
+    assert len(resp) == 4
+    assert resp[0].get("ModelCardName") == "my-fourth-model-card"
+
+    resp = client.list_model_cards(ModelCardStatus="Approved").get("ModelCardSummaries")
+    assert len(resp) == 1
+    assert resp[0].get("ModelCardName") == "my-fourth-model-card"
+
+    resp = client.list_model_cards(SortBy="Name").get("ModelCardSummaries")
+    assert len(resp) == 4
+    assert resp[-1].get("ModelCardName") == "my-third-model-card"
+
+    resp = client.list_model_cards(CreationTimeBefore=datetime_now).get(
+        "ModelCardSummaries"
+    )
+    assert len(resp) == 3
+    assert resp[-1].get("ModelCardName") == "my-third-model-card"
+
+    resp = client.list_model_cards(CreationTimeAfter=datetime_now).get(
+        "ModelCardSummaries"
+    )
+    assert len(resp) == 1
+    assert resp[0].get("ModelCardName") == "my-fourth-model-card"
+
+
+@mock_aws
+def test_list_model_card_versions_basic():
+    client = boto3.client("sagemaker", region_name="us-east-1")
+    client.create_model_card(
+        ModelCardName="my-first-model-card",
+        ModelCardStatus="Draft",
+        Content='{"model_overview": {"model_description": "my first model"}}',
+    )
+    time.sleep(1)
+    client.update_model_card(
+        ModelCardName="my-first-model-card",
+        ModelCardStatus="Draft",
+        Content='{"model_overview": {"model_description": "my first model. :))"}}',
+    )
+    resp = client.list_model_card_versions(ModelCardName="my-first-model-card").get(
+        "ModelCardVersionSummaryList"
+    )
+    assert len(resp) == 2, f"Expected 2 model card versions, found {len(resp)}"
+    assert resp[0].get("ModelCardVersion") == 1
+    assert resp[1].get("ModelCardVersion") == 2
+    assert resp[0].get("CreationTime") == resp[1].get("CreationTime")
+    assert resp[0].get("LastModifiedTime") < resp[1].get("LastModifiedTime")  # type: ignore
+
+
+@mock_aws
+def test_list_model_card_versions_advanced():
+    client = boto3.client("sagemaker", region_name="us-east-1")
+    client.create_model_card(
+        ModelCardName="my-first-model-card",
+        ModelCardStatus="Draft",
+        Content='{"model_overview": {"model_description": "my first model"}}',
+    )
+    client.update_model_card(
+        ModelCardName="my-first-model-card",
+        ModelCardStatus="Draft",
+        Content='{"model_overview": {"model_description": "my first model. :))"}}',
+    )
+    client.update_model_card(
+        ModelCardName="my-first-model-card",
+        ModelCardStatus="Approved",
+        Content='{"model_overview": {"model_description": "my first model. :))"}}',
+    )
+    client.create_model_card(
+        ModelCardName="my-second-model-card",
+        ModelCardStatus="Draft",
+        Content='{"model_overview": {"model_description": "my first model. :))"}}',
+    )
+
+    resp = client.list_model_card_versions(ModelCardName="my-first-model-card").get(
+        "ModelCardVersionSummaryList"
+    )
+    assert len(resp) == 3
+    assert [r.get("ModelCardVersion") for r in resp] == [1, 2, 3]
+
+    resp = client.list_model_card_versions(
+        ModelCardName="my-first-model-card", SortOrder="Descending"
+    ).get("ModelCardVersionSummaryList")
+    assert len(resp) == 3
+    assert [r.get("ModelCardVersion") for r in resp] == [3, 2, 1]
+
+    resp = client.list_model_card_versions(
+        ModelCardName="my-first-model-card", ModelCardStatus="Approved"
+    ).get("ModelCardVersionSummaryList")
+    assert len(resp) == 1
+    assert [r.get("ModelCardVersion") for r in resp] == [3]
+
+    resp = client.list_model_card_versions(ModelCardName="my-second-model-card").get(
+        "ModelCardVersionSummaryList"
+    )
+    assert len(resp) == 1
+
+    with pytest.raises(ClientError) as e:
+        client.list_model_card_versions(ModelCardName="I-dont-exist")
+    assert e.value.response["Error"]["Code"] == "ResourceNotFound"
+
+
+@mock_aws
+def test_describe_model_card():
+    client = boto3.client("sagemaker", region_name="us-east-1")
+
+    client.create_model_card(
+        ModelCardName="my-first-model-card",
+        SecurityConfig={"KmsKeyId": "1234abcd-12ab-34cd-56ef-1234567890ab"},
+        ModelCardStatus="Draft",
+        Content='{"model_overview": {"model_description": "my first model"}}',
+    )
+
+    resp = client.describe_model_card(ModelCardName="my-first-model-card")
+    assert (
+        resp["ModelCardArn"]
+        == f"arn:aws:sagemaker:us-east-1:{ACCOUNT_ID}:model-card/my-first-model-card"
+    )
+    assert resp["ModelCardName"] == "my-first-model-card"
+    assert resp["ModelCardVersion"] == 1
+    assert (
+        resp["Content"] == '{"model_overview": {"model_description": "my first model"}}'
+    )
+    assert resp["ModelCardStatus"] == "Draft"
+    assert resp["SecurityConfig"] == {
+        "KmsKeyId": "1234abcd-12ab-34cd-56ef-1234567890ab"
+    }
+    assert resp["CreatedBy"] == {}
+    assert resp["LastModifiedBy"] == {}
+
+    client.update_model_card(
+        ModelCardName="my-first-model-card",
+        Content='{"model_overview": {"model_description": "my first model >:)"}}',
+        ModelCardStatus="Approved",
+    )
+
+    resp = client.describe_model_card(
+        ModelCardName="my-first-model-card", ModelCardVersion=1
+    )
+
+    assert (
+        resp["ModelCardArn"]
+        == f"arn:aws:sagemaker:us-east-1:{ACCOUNT_ID}:model-card/my-first-model-card"
+    )
+    assert resp["ModelCardName"] == "my-first-model-card"
+    assert resp["ModelCardVersion"] == 1
+    assert (
+        resp["Content"] == '{"model_overview": {"model_description": "my first model"}}'
+    )
+    assert resp["ModelCardStatus"] == "Draft"
+    assert resp["SecurityConfig"] == {
+        "KmsKeyId": "1234abcd-12ab-34cd-56ef-1234567890ab"
+    }
+    assert resp["CreatedBy"] == {}
+    assert resp["LastModifiedBy"] == {}
+
+    resp = client.describe_model_card(ModelCardName="my-first-model-card")
+
+    assert (
+        resp["ModelCardArn"]
+        == f"arn:aws:sagemaker:us-east-1:{ACCOUNT_ID}:model-card/my-first-model-card"
+    )
+    assert resp["ModelCardName"] == "my-first-model-card"
+    assert resp["ModelCardVersion"] == 2
+    assert (
+        resp["Content"]
+        == '{"model_overview": {"model_description": "my first model >:)"}}'
+    )
+    assert resp["ModelCardStatus"] == "Approved"
+    assert resp["SecurityConfig"] == {
+        "KmsKeyId": "1234abcd-12ab-34cd-56ef-1234567890ab"
+    }
+    assert resp["CreatedBy"] == {}
+    assert resp["LastModifiedBy"] == {}
+
+    resp = client.describe_model_card(
+        ModelCardName="my-first-model-card", ModelCardVersion=2
+    )
+
+    assert (
+        resp["ModelCardArn"]
+        == f"arn:aws:sagemaker:us-east-1:{ACCOUNT_ID}:model-card/my-first-model-card"
+    )
+    assert resp["ModelCardName"] == "my-first-model-card"
+    assert resp["ModelCardVersion"] == 2
+    assert (
+        resp["Content"]
+        == '{"model_overview": {"model_description": "my first model >:)"}}'
+    )
+    assert resp["ModelCardStatus"] == "Approved"
+    assert resp["SecurityConfig"] == {
+        "KmsKeyId": "1234abcd-12ab-34cd-56ef-1234567890ab"
+    }
+    assert resp["CreatedBy"] == {}
+    assert resp["LastModifiedBy"] == {}
+
+    with pytest.raises(ClientError) as e:
+        client.describe_model_card(
+            ModelCardName="my-first-model-card", ModelCardVersion=3
+        )
+    assert e.value.response["Error"]["Code"] == "ResourceNotFound"
+    assert (
+        e.value.response["Error"]["Message"]
+        == "Modelcard with name my-first-model-card and version: 3 does not exist"
+    )
+
+
+@mock_aws
+def test_delete_model_card():
+    client = boto3.client("sagemaker", region_name="eu-west-1")
+
+    _ = client.create_model_card(
+        ModelCardName="my-first-model-card",
+        ModelCardStatus="Draft",
+        Content='{"model_overview": {"model_description": f"my first model"}}',
+    )
+
+    _ = client.delete_model_card(ModelCardName="my-first-model-card")
+    resp = client.list_model_cards().get("ModelCardSummaries")
+    assert len(resp) == 0
+
+    with pytest.raises(ClientError) as e:
+        client.delete_model_card(ModelCardName="my-first-model-card")
+    assert e.value.response["Error"]["Code"] == "ResourceNotFound"