Repository: getmoto/moto (python)

Issue:
Plans on Cost Explorer's get_cost_and_usage
Hello! I am aware Cost Explorer is available now in moto, but only for [certain features](http://docs.getmoto.org/en/latest/docs/services/ce.html?highlight=cost%20explorer#moto.ce.models.CostExplorerBackend).
So, I have a quick question, are there any plans on mocking Cost Explorer's **get_cost_and_usage**?

Pull request:
CostExplorer - get_cost_and_usage()
Fixes #5596 

Patch:
diff --git a/IMPLEMENTATION_COVERAGE.md b/IMPLEMENTATION_COVERAGE.md
index a54cb2956..c51d2e8d7 100644
--- a/IMPLEMENTATION_COVERAGE.md
+++ b/IMPLEMENTATION_COVERAGE.md
@@ -735,7 +735,7 @@
 
 ## ce
 <details>
-<summary>18% implemented</summary>
+<summary>21% implemented</summary>
 
 - [ ] create_anomaly_monitor
 - [ ] create_anomaly_subscription
@@ -747,7 +747,7 @@
 - [ ] get_anomalies
 - [ ] get_anomaly_monitors
 - [ ] get_anomaly_subscriptions
-- [ ] get_cost_and_usage
+- [X] get_cost_and_usage
 - [ ] get_cost_and_usage_with_resources
 - [ ] get_cost_categories
 - [ ] get_cost_forecast
diff --git a/docs/docs/services/ce.rst b/docs/docs/services/ce.rst
index 77d77b532..bd609ebb3 100644
--- a/docs/docs/services/ce.rst
+++ b/docs/docs/services/ce.rst
@@ -38,7 +38,50 @@ ce
 - [ ] get_anomalies
 - [ ] get_anomaly_monitors
 - [ ] get_anomaly_subscriptions
-- [ ] get_cost_and_usage
+- [X] get_cost_and_usage
+  
+        There is no validation yet on any of the input parameters.
+
+        Cost or usage is not tracked by Moto, so this call will return nothing by default.
+
+        You can use a dedicated API to override this, by configuring a queue of expected results.
+
+        A request to `get_cost_and_usage` will take the first result from that queue, and assign it to the provided parameters. Subsequent requests using the same parameters will return the same result. Other requests using different parameters will take the next result from the queue, or return an empty result if the queue is empty.
+
+        Configure this queue by making an HTTP request to `/moto-api/static/ce/cost-and-usage-results`. An example invocation looks like this:
+
+        .. sourcecode:: python
+
+            result = {
+                "results": [
+                    {
+                        "ResultsByTime": [
+                            {
+                                "TimePeriod": {"Start": "2024-01-01", "End": "2024-01-02"},
+                                "Total": {
+                                    "BlendedCost": {"Amount": "0.0101516483", "Unit": "USD"}
+                                },
+                                "Groups": [],
+                                "Estimated": False
+                            }
+                        ],
+                        "DimensionValueAttributes": [{"Value": "v", "Attributes": {"a": "b"}}]
+                    },
+                    {
+                        ...
+                    },
+                ]
+            }
+            resp = requests.post(
+                "http://motoapi.amazonaws.com:5000/moto-api/static/ce/cost-and-usage-results",
+                json=expected_results,
+            )
+            assert resp.status_code == 201
+
+            ce = boto3.client("ce", region_name="us-east-1")
+            resp = ce.get_cost_and_usage(...)
+        
+
 - [ ] get_cost_and_usage_with_resources
 - [ ] get_cost_categories
 - [ ] get_cost_forecast
diff --git a/moto/ce/models.py b/moto/ce/models.py
index 97938e303..add58e3c0 100644
--- a/moto/ce/models.py
+++ b/moto/ce/models.py
@@ -74,6 +74,8 @@ class CostExplorerBackend(BaseBackend):
     def __init__(self, region_name: str, account_id: str):
         super().__init__(region_name, account_id)
         self.cost_categories: Dict[str, CostCategoryDefinition] = dict()
+        self.cost_usage_results_queue: List[Dict[str, Any]] = []
+        self.cost_usage_results: Dict[str, Dict[str, Any]] = {}
         self.tagger = TaggingService()
 
     def create_cost_category_definition(
@@ -154,6 +156,57 @@ class CostExplorerBackend(BaseBackend):
     def untag_resource(self, resource_arn: str, tag_keys: List[str]) -> None:
         self.tagger.untag_resource_using_names(resource_arn, tag_keys)
 
+    def get_cost_and_usage(self, body: str) -> Dict[str, Any]:
+        """
+        There is no validation yet on any of the input parameters.
+
+        Cost or usage is not tracked by Moto, so this call will return nothing by default.
+
+        You can use a dedicated API to override this, by configuring a queue of expected results.
+
+        A request to `get_cost_and_usage` will take the first result from that queue, and assign it to the provided parameters. Subsequent requests using the same parameters will return the same result. Other requests using different parameters will take the next result from the queue, or return an empty result if the queue is empty.
+
+        Configure this queue by making an HTTP request to `/moto-api/static/ce/cost-and-usage-results`. An example invocation looks like this:
+
+        .. sourcecode:: python
+
+            result = {
+                "results": [
+                    {
+                        "ResultsByTime": [
+                            {
+                                "TimePeriod": {"Start": "2024-01-01", "End": "2024-01-02"},
+                                "Total": {
+                                    "BlendedCost": {"Amount": "0.0101516483", "Unit": "USD"}
+                                },
+                                "Groups": [],
+                                "Estimated": False
+                            }
+                        ],
+                        "DimensionValueAttributes": [{"Value": "v", "Attributes": {"a": "b"}}]
+                    },
+                    {
+                        ...
+                    },
+                ]
+            }
+            resp = requests.post(
+                "http://motoapi.amazonaws.com:5000/moto-api/static/ce/cost-and-usage-results",
+                json=expected_results,
+            )
+            assert resp.status_code == 201
+
+            ce = boto3.client("ce", region_name="us-east-1")
+            resp = ce.get_cost_and_usage(...)
+        """
+        default_result: Dict[str, Any] = {
+            "ResultsByTime": [],
+            "DimensionValueAttributes": [],
+        }
+        if body not in self.cost_usage_results and self.cost_usage_results_queue:
+            self.cost_usage_results[body] = self.cost_usage_results_queue.pop(0)
+        return self.cost_usage_results.get(body, default_result)
+
 
 ce_backends = BackendDict(
     CostExplorerBackend, "ce", use_boto3_regions=False, additional_regions=["global"]
diff --git a/moto/ce/responses.py b/moto/ce/responses.py
index 45770478c..9ade12190 100644
--- a/moto/ce/responses.py
+++ b/moto/ce/responses.py
@@ -102,3 +102,7 @@ class CostExplorerResponse(BaseResponse):
         tag_names = params.get("ResourceTagKeys")
         self.ce_backend.untag_resource(resource_arn, tag_names)
         return json.dumps({})
+
+    def get_cost_and_usage(self) -> str:
+        resp = self.ce_backend.get_cost_and_usage(self.body)
+        return json.dumps(resp)
diff --git a/moto/moto_api/_internal/models.py b/moto/moto_api/_internal/models.py
index b67e9533f..28f1fdc03 100644
--- a/moto/moto_api/_internal/models.py
+++ b/moto/moto_api/_internal/models.py
@@ -42,6 +42,12 @@ class MotoAPIBackend(BaseBackend):
         results = QueryResults(rows=rows, column_info=column_info)
         backend.query_results_queue.append(results)
 
+    def set_ce_cost_usage(self, result: Dict[str, Any], account_id: str) -> None:
+        from moto.ce.models import ce_backends
+
+        backend = ce_backends[account_id]["global"]
+        backend.cost_usage_results_queue.append(result)
+
     def set_sagemaker_result(
         self,
         body: str,
diff --git a/moto/moto_api/_internal/responses.py b/moto/moto_api/_internal/responses.py
index 8302d0077..66c0bcf54 100644
--- a/moto/moto_api/_internal/responses.py
+++ b/moto/moto_api/_internal/responses.py
@@ -168,6 +168,23 @@ class MotoAPIResponse(BaseResponse):
             )
         return 201, {}, ""
 
+    def set_ce_cost_usage_result(
+        self,
+        request: Any,
+        full_url: str,  # pylint: disable=unused-argument
+        headers: Any,
+    ) -> TYPE_RESPONSE:
+        from .models import moto_api_backend
+
+        request_body_size = int(headers["Content-Length"])
+        body = request.environ["wsgi.input"].read(request_body_size).decode("utf-8")
+        body = json.loads(body)
+        account_id = body.get("account_id", DEFAULT_ACCOUNT_ID)
+
+        for result in body.get("results", []):
+            moto_api_backend.set_ce_cost_usage(result=result, account_id=account_id)
+        return 201, {}, ""
+
     def set_sagemaker_result(
         self,
         request: Any,
diff --git a/moto/moto_api/_internal/urls.py b/moto/moto_api/_internal/urls.py
index 987a4278c..d9f0d243d 100644
--- a/moto/moto_api/_internal/urls.py
+++ b/moto/moto_api/_internal/urls.py
@@ -13,6 +13,7 @@ url_paths = {
     "{0}/moto-api/reset-auth": response_instance.reset_auth_response,
     "{0}/moto-api/seed": response_instance.seed,
     "{0}/moto-api/static/athena/query-results": response_instance.set_athena_result,
+    "{0}/moto-api/static/ce/cost-and-usage-results": response_instance.set_ce_cost_usage_result,
     "{0}/moto-api/static/inspector2/findings-results": response_instance.set_inspector2_findings_result,
     "{0}/moto-api/static/sagemaker/endpoint-results": response_instance.set_sagemaker_result,
     "{0}/moto-api/static/rds-data/statement-results": response_instance.set_rds_data_result,


Test patch:
diff --git a/tests/test_ce/test_ce_cost_and_usage.py b/tests/test_ce/test_ce_cost_and_usage.py
new file mode 100644
index 000000000..584d38134
--- /dev/null
+++ b/tests/test_ce/test_ce_cost_and_usage.py
@@ -0,0 +1,64 @@
+import boto3
+import requests
+
+from moto import mock_aws, settings
+
+# See our Development Tips on writing tests for hints on how to write good tests:
+# http://docs.getmoto.org/en/latest/docs/contributing/development_tips/tests.html
+
+
+@mock_aws
+def test_get_cost_and_usage():
+    ce = boto3.client("ce", region_name="eu-west-1")
+
+    resp = ce.get_cost_and_usage(
+        TimePeriod={"Start": "2024-01-01", "End": "2024-02-01"},
+        Granularity="DAILY",
+        Metrics=["UNBLENDEDCOST"],
+    )
+
+    assert resp["DimensionValueAttributes"] == []
+    assert resp["ResultsByTime"] == []
+
+
+@mock_aws
+def test_set_query_results():
+    base_url = (
+        settings.test_server_mode_endpoint()
+        if settings.TEST_SERVER_MODE
+        else "http://motoapi.amazonaws.com"
+    )
+
+    cost_and_usage_result = {
+        "results": [
+            {
+                "ResultsByTime": [
+                    {
+                        "TimePeriod": {"Start": "2024-01-01", "End": "2024-01-02"},
+                        "Total": {
+                            "BlendedCost": {"Amount": "0.0101516483", "Unit": "USD"}
+                        },
+                        "Groups": [],
+                        "Estimated": False,
+                    }
+                ],
+                "DimensionValueAttributes": [{"Value": "v", "Attributes": {"a": "b"}}],
+            }
+        ],
+    }
+    resp = requests.post(
+        f"{base_url}/moto-api/static/ce/cost-and-usage-results",
+        json=cost_and_usage_result,
+    )
+    assert resp.status_code == 201
+
+    ce = boto3.client("ce", region_name="us-west-1")
+
+    resp = ce.get_cost_and_usage(
+        TimePeriod={"Start": "2024-01-01", "End": "2024-02-01"},
+        Granularity="DAILY",
+        Metrics=["BLENDEDCOST", "AMORTIZEDCOST"],
+    )
+
+    resp.pop("ResponseMetadata")
+    assert resp == cost_and_usage_result["results"][0]