Repository: getmoto/moto
Pull request #5972 (2023-02-24 10:33:39)

Techdebt: MyPy ElasticBeanstalk

codecov[bot]: # [Codecov](https://codecov.io/gh/getmoto/moto/pull/5972?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) Report
> Merging [#5972](https://codecov.io/gh/getmoto/moto/pull/5972?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) (f309fd0) into [master](https://codecov.io/gh/getmoto/moto/commit/4d6271d1bdec1c5eec5789d7c6cc8ef35566d0c2?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) (4d6271d) will **increase** coverage by `0.00%`.
> The diff coverage is `96.96%`.

```diff
@@           Coverage Diff           @@
##           master    #5972   +/-   ##
=======================================
  Coverage   96.22%   96.22%           
=======================================
  Files         760      760           
  Lines       74761    74762    +1     
=======================================
+ Hits        71939    71943    +4     
+ Misses       2822     2819    -3     
```

| Flag | Coverage Δ | |
|---|---|---|
| servertests | `37.00% <63.63%> (+<0.01%)` | :arrow_up: |
| unittests | `96.17% <96.96%> (+<0.01%)` | :arrow_up: |

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

| [Impacted Files](https://codecov.io/gh/getmoto/moto/pull/5972?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) | Coverage Δ | |
|---|---|---|
| [moto/elasticbeanstalk/models.py](https://codecov.io/gh/getmoto/moto/pull/5972?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#diff-bW90by9lbGFzdGljYmVhbnN0YWxrL21vZGVscy5weQ==) | `91.56% <95.00%> (+0.10%)` | :arrow_up: |
| [moto/elasticbeanstalk/exceptions.py](https://codecov.io/gh/getmoto/moto/pull/5972?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#diff-bW90by9lbGFzdGljYmVhbnN0YWxrL2V4Y2VwdGlvbnMucHk=) | `85.71% <100.00%> (ø)` | |
| [moto/elasticbeanstalk/responses.py](https://codecov.io/gh/getmoto/moto/pull/5972?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#diff-bW90by9lbGFzdGljYmVhbnN0YWxrL3Jlc3BvbnNlcy5weQ==) | `96.07% <100.00%> (ø)` | |
| [moto/elasticbeanstalk/utils.py](https://codecov.io/gh/getmoto/moto/pull/5972?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#diff-bW90by9lbGFzdGljYmVhbnN0YWxrL3V0aWxzLnB5) | `100.00% <100.00%> (ø)` | |
| [moto/batch/models.py](https://codecov.io/gh/getmoto/moto/pull/5972?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#diff-bW90by9iYXRjaC9tb2RlbHMucHk=) | `91.19% <0.00%> (+0.35%)` | :arrow_up: |

Help us with your feedback. Take ten seconds to tell us [how you rate us](https://about.codecov.io/nps?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None). Have a feature suggestion? [Share it here.](https://app.codecov.io/gh/feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None)


Patch:
diff --git a/IMPLEMENTATION_COVERAGE.md b/IMPLEMENTATION_COVERAGE.md
index b97cab8a5..c602ce7b1 100644
--- a/IMPLEMENTATION_COVERAGE.md
+++ b/IMPLEMENTATION_COVERAGE.md
@@ -3100,7 +3100,7 @@
 
 ## glue
 <details>
-<summary>21% implemented</summary>
+<summary>23% implemented</summary>
 
 - [X] batch_create_partition
 - [ ] batch_delete_connection
@@ -3165,7 +3165,7 @@
 - [ ] delete_security_configuration
 - [ ] delete_session
 - [X] delete_table
-- [ ] delete_table_version
+- [X] delete_table_version
 - [ ] delete_trigger
 - [ ] delete_user_defined_function
 - [ ] delete_workflow
@@ -3219,8 +3219,8 @@
 - [ ] get_session
 - [ ] get_statement
 - [X] get_table
-- [ ] get_table_version
-- [ ] get_table_versions
+- [X] get_table_version
+- [X] get_table_versions
 - [X] get_tables
 - [X] get_tags
 - [ ] get_trigger
diff --git a/docs/docs/services/athena.rst b/docs/docs/services/athena.rst
index 7e99f7980..5d8849150 100644
--- a/docs/docs/services/athena.rst
+++ b/docs/docs/services/athena.rst
@@ -51,33 +51,49 @@ athena
 - [X] get_query_execution
 - [X] get_query_results
   
-        Queries are not executed, so this call will always return 0 rows by default.
+        Queries are not executed by Moto, so this call will always return 0 rows by default.
 
-        When using decorators, you can use the internal API to manually set results:
+        You can use a dedicated API to configure this. Moto has a queue that can be filled with the expected results.
+
+        A request to `get_query_results` will take the first result from that queue, and assign it to the provided QueryExecutionId. Subsequent requests using the same QueryExecutionId will return the same result. Other requests using a different QueryExecutionId will take the next result from the queue, or return an empty result if the queue is empty.
+
+        Configuring this queue by making a HTTP request to `/moto-api/static/athena/query-results`. An example invocation looks like this:
 
         .. sourcecode:: python
 
-            from moto.athena.models import athena_backends, QueryResults
-            from moto.core import DEFAULT_ACCOUNT_ID
-
-            backend = athena_backends[DEFAULT_ACCOUNT_ID]["us-east-1"]
-            rows = [{'Data': [{'VarCharValue': '..'}]}]
-            column_info = [{
-                              'CatalogName': 'string',
-                              'SchemaName': 'string',
-                              'TableName': 'string',
-                              'Name': 'string',
-                              'Label': 'string',
-                              'Type': 'string',
-                              'Precision': 123,
-                              'Scale': 123,
-                              'Nullable': 'NOT_NULL',
-                              'CaseSensitive': True
-                          }]
-            results = QueryResults(rows=rows, column_info=column_info)
-            backend.query_results["test"] = results
-
-            result = client.get_query_results(QueryExecutionId="test")
+            expected_results = {
+                "account_id": "123456789012",  # This is the default - can be omitted
+                "region": "us-east-1",  # This is the default - can be omitted
+                "results": [
+                    {
+                        "rows": [{"Data": [{"VarCharValue": "1"}]}],
+                        "column_info": [{
+                            "CatalogName": "string",
+                            "SchemaName": "string",
+                            "TableName": "string",
+                            "Name": "string",
+                            "Label": "string",
+                            "Type": "string",
+                            "Precision": 123,
+                            "Scale": 123,
+                            "Nullable": "NOT_NULL",
+                            "CaseSensitive": True,
+                        }],
+                    },
+                    # other results as required
+                ],
+            }
+            resp = requests.post(
+                "http://motoapi.amazonaws.com:5000/moto-api/static/athena/query-results",
+                json=athena_result,
+            )
+            resp.status_code.should.equal(201)
+
+            client = boto3.client("athena", region_name="us-east-1")
+            details = client.get_query_execution(QueryExecutionId="any_id")["QueryExecution"]
+
+        .. note:: The exact QueryExecutionId is not relevant here, but will likely be whatever value is returned by start_query_execution
+
         
 
 - [ ] get_query_runtime_statistics
diff --git a/docs/docs/services/glue.rst b/docs/docs/services/glue.rst
index d33d925bc..219f6b80c 100644
--- a/docs/docs/services/glue.rst
+++ b/docs/docs/services/glue.rst
@@ -92,7 +92,7 @@ glue
 - [ ] delete_security_configuration
 - [ ] delete_session
 - [X] delete_table
-- [ ] delete_table_version
+- [X] delete_table_version
 - [ ] delete_trigger
 - [ ] delete_user_defined_function
 - [ ] delete_workflow
@@ -158,8 +158,8 @@ glue
 - [ ] get_session
 - [ ] get_statement
 - [X] get_table
-- [ ] get_table_version
-- [ ] get_table_versions
+- [X] get_table_version
+- [X] get_table_versions
 - [X] get_tables
 - [X] get_tags
 - [ ] get_trigger
diff --git a/moto/athena/models.py b/moto/athena/models.py
index bfd2d9205..975bddb3a 100644
--- a/moto/athena/models.py
+++ b/moto/athena/models.py
@@ -132,6 +132,7 @@ class AthenaBackend(BaseBackend):
         self.named_queries: Dict[str, NamedQuery] = {}
         self.data_catalogs: Dict[str, DataCatalog] = {}
         self.query_results: Dict[str, QueryResults] = {}
+        self.query_results_queue: List[QueryResults] = []
 
     @staticmethod
     def default_vpc_endpoint_service(
@@ -195,34 +196,52 @@ class AthenaBackend(BaseBackend):
 
     def get_query_results(self, exec_id: str) -> QueryResults:
         """
-        Queries are not executed, so this call will always return 0 rows by default.
+        Queries are not executed by Moto, so this call will always return 0 rows by default.
 
-        When using decorators, you can use the internal API to manually set results:
+        You can use a dedicated API to configure this. Moto has a queue that can be filled with the expected results.
+
+        A request to `get_query_results` will take the first result from that queue, and assign it to the provided QueryExecutionId. Subsequent requests using the same QueryExecutionId will return the same result. Other requests using a different QueryExecutionId will take the next result from the queue, or return an empty result if the queue is empty.
+
+        Configuring this queue by making a HTTP request to `/moto-api/static/athena/query-results`. An example invocation looks like this:
 
         .. sourcecode:: python
 
-            from moto.athena.models import athena_backends, QueryResults
-            from moto.core import DEFAULT_ACCOUNT_ID
-
-            backend = athena_backends[DEFAULT_ACCOUNT_ID]["us-east-1"]
-            rows = [{'Data': [{'VarCharValue': '..'}]}]
-            column_info = [{
-                              'CatalogName': 'string',
-                              'SchemaName': 'string',
-                              'TableName': 'string',
-                              'Name': 'string',
-                              'Label': 'string',
-                              'Type': 'string',
-                              'Precision': 123,
-                              'Scale': 123,
-                              'Nullable': 'NOT_NULL',
-                              'CaseSensitive': True
-                          }]
-            results = QueryResults(rows=rows, column_info=column_info)
-            backend.query_results["test"] = results
-
-            result = client.get_query_results(QueryExecutionId="test")
+            expected_results = {
+                "account_id": "123456789012",  # This is the default - can be omitted
+                "region": "us-east-1",  # This is the default - can be omitted
+                "results": [
+                    {
+                        "rows": [{"Data": [{"VarCharValue": "1"}]}],
+                        "column_info": [{
+                            "CatalogName": "string",
+                            "SchemaName": "string",
+                            "TableName": "string",
+                            "Name": "string",
+                            "Label": "string",
+                            "Type": "string",
+                            "Precision": 123,
+                            "Scale": 123,
+                            "Nullable": "NOT_NULL",
+                            "CaseSensitive": True,
+                        }],
+                    },
+                    # other results as required
+                ],
+            }
+            resp = requests.post(
+                "http://motoapi.amazonaws.com:5000/moto-api/static/athena/query-results",
+                json=athena_result,
+            )
+            resp.status_code.should.equal(201)
+
+            client = boto3.client("athena", region_name="us-east-1")
+            details = client.get_query_execution(QueryExecutionId="any_id")["QueryExecution"]
+
+        .. note:: The exact QueryExecutionId is not relevant here, but will likely be whatever value is returned by start_query_execution
+
         """
+        if exec_id not in self.query_results and self.query_results_queue:
+            self.query_results[exec_id] = self.query_results_queue.pop(0)
         results = (
             self.query_results[exec_id]
             if exec_id in self.query_results
diff --git a/moto/elasticbeanstalk/exceptions.py b/moto/elasticbeanstalk/exceptions.py
index d880fa001..22e70cb29 100644
--- a/moto/elasticbeanstalk/exceptions.py
+++ b/moto/elasticbeanstalk/exceptions.py
@@ -2,10 +2,10 @@ from moto.core.exceptions import RESTError
 
 
 class InvalidParameterValueError(RESTError):
-    def __init__(self, message):
+    def __init__(self, message: str):
         super().__init__("InvalidParameterValue", message)
 
 
 class ResourceNotFoundException(RESTError):
-    def __init__(self, message):
+    def __init__(self, message: str):
         super().__init__("ResourceNotFoundException", message)
diff --git a/moto/elasticbeanstalk/models.py b/moto/elasticbeanstalk/models.py
index f984346a9..e809c8cf6 100644
--- a/moto/elasticbeanstalk/models.py
+++ b/moto/elasticbeanstalk/models.py
@@ -1,4 +1,5 @@
 import weakref
+from typing import Dict, List
 
 from moto.core import BaseBackend, BackendDict, BaseModel
 from .exceptions import InvalidParameterValueError, ResourceNotFoundException
@@ -6,7 +7,13 @@ from .utils import make_arn
 
 
 class FakeEnvironment(BaseModel):
-    def __init__(self, application, environment_name, solution_stack_name, tags):
+    def __init__(
+        self,
+        application: "FakeApplication",
+        environment_name: str,
+        solution_stack_name: str,
+        tags: Dict[str, str],
+    ):
         self.application = weakref.proxy(
             application
         )  # weakref to break circular dependencies
@@ -15,39 +22,41 @@ class FakeEnvironment(BaseModel):
         self.tags = tags
 
     @property
-    def application_name(self):
+    def application_name(self) -> str:
         return self.application.application_name
 
     @property
-    def environment_arn(self):
+    def environment_arn(self) -> str:
         resource_path = f"{self.application_name}/{self.environment_name}"
         return make_arn(
             self.region, self.application.account_id, "environment", resource_path
         )
 
     @property
-    def platform_arn(self):
+    def platform_arn(self) -> str:
         return "TODO"  # TODO
 
     @property
-    def region(self):
+    def region(self) -> str:
         return self.application.region
 
 
 class FakeApplication(BaseModel):
-    def __init__(self, backend, application_name):
+    def __init__(self, backend: "EBBackend", application_name: str):
         self.backend = weakref.proxy(backend)  # weakref to break cycles
         self.application_name = application_name
-        self.environments = dict()
+        self.environments: Dict[str, FakeEnvironment] = dict()
         self.account_id = self.backend.account_id
         self.region = self.backend.region_name
         self.arn = make_arn(
             self.region, self.account_id, "application", self.application_name
         )
 
-    def create_environment(self, environment_name, solution_stack_name, tags):
+    def create_environment(
+        self, environment_name: str, solution_stack_name: str, tags: Dict[str, str]
+    ) -> FakeEnvironment:
         if environment_name in self.environments:
-            raise InvalidParameterValueError
+            raise InvalidParameterValueError(message="")
 
         env = FakeEnvironment(
             application=self,
@@ -61,12 +70,14 @@ class FakeApplication(BaseModel):
 
 
 class EBBackend(BaseBackend):
-    def __init__(self, region_name, account_id):
+    def __init__(self, region_name: str, account_id: str):
         super().__init__(region_name, account_id)
-        self.applications = dict()
+        self.applications: Dict[str, FakeApplication] = dict()
 
     @staticmethod
-    def default_vpc_endpoint_service(service_region, zones):
+    def default_vpc_endpoint_service(
+        service_region: str, zones: List[str]
+    ) -> List[Dict[str, str]]:
         """Default VPC endpoint service."""
         return BaseBackend.default_vpc_endpoint_service_factory(
             service_region, zones, "elasticbeanstalk"
@@ -74,7 +85,7 @@ class EBBackend(BaseBackend):
             service_region, zones, "elasticbeanstalk-health"
         )
 
-    def create_application(self, application_name):
+    def create_application(self, application_name: str) -> FakeApplication:
         if application_name in self.applications:
             raise InvalidParameterValueError(
                 f"Application {application_name} already exists."
@@ -83,23 +94,31 @@ class EBBackend(BaseBackend):
         self.applications[application_name] = new_app
         return new_app
 
-    def create_environment(self, app, environment_name, stack_name, tags):
+    def create_environment(
+        self,
+        app: FakeApplication,
+        environment_name: str,
+        stack_name: str,
+        tags: Dict[str, str],
+    ) -> FakeEnvironment:
         return app.create_environment(
             environment_name=environment_name, solution_stack_name=stack_name, tags=tags
         )
 
-    def describe_environments(self):
+    def describe_environments(self) -> List[FakeEnvironment]:
         envs = []
         for app in self.applications.values():
             for env in app.environments.values():
                 envs.append(env)
         return envs
 
-    def list_available_solution_stacks(self):
+    def list_available_solution_stacks(self) -> None:
         # Implemented in response.py
         pass
 
-    def update_tags_for_resource(self, resource_arn, tags_to_add, tags_to_remove):
+    def update_tags_for_resource(
+        self, resource_arn: str, tags_to_add: Dict[str, str], tags_to_remove: List[str]
+    ) -> None:
         try:
             res = self._find_environment_by_arn(resource_arn)
         except KeyError:
@@ -113,7 +132,7 @@ class EBBackend(BaseBackend):
         for key in tags_to_remove:
             del res.tags[key]
 
-    def list_tags_for_resource(self, resource_arn):
+    def list_tags_for_resource(self, resource_arn: str) -> Dict[str, str]:
         try:
             res = self._find_environment_by_arn(resource_arn)
         except KeyError:
@@ -122,7 +141,7 @@ class EBBackend(BaseBackend):
             )
         return res.tags
 
-    def _find_environment_by_arn(self, arn):
+    def _find_environment_by_arn(self, arn: str) -> FakeEnvironment:
         for app in self.applications.keys():
             for env in self.applications[app].environments.values():
                 if env.environment_arn == arn:
diff --git a/moto/elasticbeanstalk/responses.py b/moto/elasticbeanstalk/responses.py
index 74d345a6c..568ed4c16 100644
--- a/moto/elasticbeanstalk/responses.py
+++ b/moto/elasticbeanstalk/responses.py
@@ -1,21 +1,21 @@
 from moto.core.responses import BaseResponse
 from moto.core.utils import tags_from_query_string
-from .models import eb_backends
+from .models import eb_backends, EBBackend
 from .exceptions import InvalidParameterValueError
 
 
 class EBResponse(BaseResponse):
-    def __init__(self):
+    def __init__(self) -> None:
         super().__init__(service_name="elasticbeanstalk")
 
     @property
-    def backend(self):
+    def backend(self) -> EBBackend:
         """
         :rtype: EBBackend
         """
         return eb_backends[self.current_account][self.region]
 
-    def create_application(self):
+    def create_application(self) -> str:
         app = self.backend.create_application(
             application_name=self._get_param("ApplicationName")
         )
@@ -23,11 +23,11 @@ class EBResponse(BaseResponse):
         template = self.response_template(EB_CREATE_APPLICATION)
         return template.render(region_name=self.backend.region_name, application=app)
 
-    def describe_applications(self):
+    def describe_applications(self) -> str:
         template = self.response_template(EB_DESCRIBE_APPLICATIONS)
         return template.render(applications=self.backend.applications.values())
 
-    def create_environment(self):
+    def create_environment(self) -> str:
         application_name = self._get_param("ApplicationName")
         try:
             app = self.backend.applications[application_name]
@@ -47,16 +47,16 @@ class EBResponse(BaseResponse):
         template = self.response_template(EB_CREATE_ENVIRONMENT)
         return template.render(environment=env, region=self.backend.region_name)
 
-    def describe_environments(self):
+    def describe_environments(self) -> str:
         envs = self.backend.describe_environments()
 
         template = self.response_template(EB_DESCRIBE_ENVIRONMENTS)
         return template.render(environments=envs)
 
-    def list_available_solution_stacks(self):
+    def list_available_solution_stacks(self) -> str:
         return EB_LIST_AVAILABLE_SOLUTION_STACKS
 
-    def update_tags_for_resource(self):
+    def update_tags_for_resource(self) -> str:
         resource_arn = self._get_param("ResourceArn")
         tags_to_add = tags_from_query_string(
             self.querystring, prefix="TagsToAdd.member"
@@ -66,7 +66,7 @@ class EBResponse(BaseResponse):
 
         return EB_UPDATE_TAGS_FOR_RESOURCE
 
-    def list_tags_for_resource(self):
+    def list_tags_for_resource(self) -> str:
         resource_arn = self._get_param("ResourceArn")
         tags = self.backend.list_tags_for_resource(resource_arn)
 
diff --git a/moto/elasticbeanstalk/utils.py b/moto/elasticbeanstalk/utils.py
index e3e22a11d..e3e2e0f6b 100644
--- a/moto/elasticbeanstalk/utils.py
+++ b/moto/elasticbeanstalk/utils.py
@@ -1,4 +1,6 @@
-def make_arn(region, account_id, resource_type, resource_path):
+def make_arn(
+    region: str, account_id: str, resource_type: str, resource_path: str
+) -> str:
     arn_template = (
         "arn:aws:elasticbeanstalk:{region}:{account_id}:{resource_type}/{resource_path}"
     )
diff --git a/moto/glue/models.py b/moto/glue/models.py
index dcec934ba..70e794f49 100644
--- a/moto/glue/models.py
+++ b/moto/glue/models.py
@@ -1,8 +1,9 @@
+import json
 import time
 from collections import OrderedDict
 from datetime import datetime
 import re
-from typing import Dict, List
+from typing import Any, Dict, List
 
 from moto.core import BaseBackend, BackendDict, BaseModel
 from moto.moto_api import state_manager
@@ -169,6 +170,32 @@ class GlueBackend(BaseBackend):
         table = self.get_table(database_name, table_name)
         table.update(table_input)
 
+    def get_table_version(
+        self, database_name: str, table_name: str, ver_id: str
+    ) -> str:
+        table = self.get_table(database_name, table_name)
+
+        return json.dumps(
+            {
+                "TableVersion": {
+                    "Table": table.as_dict(version=ver_id),
+                    "VersionId": ver_id,
+                }
+            }
+        )
+
+    def get_table_versions(
+        self, database_name: str, table_name: str
+    ) -> Dict[str, Dict[str, Any]]:
+        table = self.get_table(database_name, table_name)
+        return {version: table.as_dict(version) for version in table.versions.keys()}
+
+    def delete_table_version(
+        self, database_name: str, table_name: str, version_id: str
+    ) -> None:
+        table = self.get_table(database_name, table_name)
+        table.delete_version(version_id)
+
     def get_partitions(self, database_name, table_name, expression):
         """
         See https://docs.aws.amazon.com/glue/latest/webapi/API_GetPartitions.html
@@ -779,33 +806,38 @@ class FakeTable(BaseModel):
         self.partitions = OrderedDict()
         self.created_time = datetime.utcnow()
         self.updated_time = None
-        self.versions = [table_input]
+        self._current_version = 1
+        self.versions: Dict[str, Dict[str, Any]] = {
+            str(self._current_version): table_input
+        }
 
     def update(self, table_input):
-        self.versions.append(table_input)
+        self.versions[str(self._current_version + 1)] = table_input
+        self._current_version += 1
         self.updated_time = datetime.utcnow()
 
     def get_version(self, ver):
         try:
-            if not isinstance(ver, int):
-                # "1" goes to [0]
-                ver = int(ver) - 1
+            int(ver)
         except ValueError as e:
             raise JsonRESTError("InvalidInputException", str(e))
 
         try:
             return self.versions[ver]
-        except IndexError:
+        except KeyError:
             raise VersionNotFoundException()
 
-    def as_dict(self, version=-1):
+    def delete_version(self, version_id):
+        self.versions.pop(version_id)
+
+    def as_dict(self, version=1):
         obj = {
             "DatabaseName": self.database_name,
             "Name": self.name,
             "CreateTime": unix_time(self.created_time),
-            **self.get_version(version),
+            **self.get_version(str(version)),
             # Add VersionId after we get the version-details, just to make sure that it's a valid version (int)
-            "VersionId": str(int(version) + 1),
+            "VersionId": str(version),
         }
         if self.updated_time is not None:
             obj["UpdateTime"] = unix_time(self.updated_time)
diff --git a/moto/glue/responses.py b/moto/glue/responses.py
index 3c6aa365b..fd797d6c2 100644
--- a/moto/glue/responses.py
+++ b/moto/glue/responses.py
@@ -72,13 +72,12 @@ class GlueResponse(BaseResponse):
     def get_table_versions(self):
         database_name = self.parameters.get("DatabaseName")
         table_name = self.parameters.get("TableName")
-        table = self.glue_backend.get_table(database_name, table_name)
-
+        versions = self.glue_backend.get_table_versions(database_name, table_name)
         return json.dumps(
             {
                 "TableVersions": [
-                    {"Table": table.as_dict(version=n), "VersionId": str(n + 1)}
-                    for n in range(len(table.versions))
+                    {"Table": data, "VersionId": version}
+                    for version, data in versions.items()
                 ]
             }
         )
@@ -86,17 +85,15 @@ class GlueResponse(BaseResponse):
     def get_table_version(self):
         database_name = self.parameters.get("DatabaseName")
         table_name = self.parameters.get("TableName")
-        table = self.glue_backend.get_table(database_name, table_name)
         ver_id = self.parameters.get("VersionId")
+        return self.glue_backend.get_table_version(database_name, table_name, ver_id)
 
-        return json.dumps(
-            {
-                "TableVersion": {
-                    "Table": table.as_dict(version=ver_id),
-                    "VersionId": ver_id,
-                }
-            }
-        )
+    def delete_table_version(self) -> str:
+        database_name = self.parameters.get("DatabaseName")
+        table_name = self.parameters.get("TableName")
+        version_id = self.parameters.get("VersionId")
+        self.glue_backend.delete_table_version(database_name, table_name, version_id)
+        return "{}"
 
     def get_tables(self):
         database_name = self.parameters.get("DatabaseName")
diff --git a/moto/glue/utils.py b/moto/glue/utils.py
index 4b1f45003..9849968c4 100644
--- a/moto/glue/utils.py
+++ b/moto/glue/utils.py
@@ -353,7 +353,8 @@ class PartitionFilter:
             return True
 
         warnings.warn("Expression filtering is experimental")
+        versions = list(self.fake_table.versions.values())
         return expression.eval(
-            part_keys=self.fake_table.versions[-1].get("PartitionKeys", []),
+            part_keys=versions[-1].get("PartitionKeys", []),
             part_input=fake_partition.partition_input,
         )
diff --git a/moto/kms/responses.py b/moto/kms/responses.py
index cf354d5c4..8e3d595eb 100644
--- a/moto/kms/responses.py
+++ b/moto/kms/responses.py
@@ -451,7 +451,7 @@ class KmsResponse(BaseResponse):
             "DestinationEncryptionContext", {}
         )
 
-        self._validate_cmk_id(destination_key_id)
+        self._validate_key_id(destination_key_id)
 
         (
             new_ciphertext_blob,
diff --git a/moto/moto_api/_internal/models.py b/moto/moto_api/_internal/models.py
index 3738b3fc7..373c00938 100644
--- a/moto/moto_api/_internal/models.py
+++ b/moto/moto_api/_internal/models.py
@@ -1,6 +1,6 @@
 from moto.core import BaseBackend, DEFAULT_ACCOUNT_ID
 from moto.core.model_instances import reset_model_data
-from typing import Any, Dict
+from typing import Any, Dict, List
 
 
 class MotoAPIBackend(BaseBackend):
@@ -33,5 +33,18 @@ class MotoAPIBackend(BaseBackend):
 
         state_manager.unset_transition(model_name)
 
+    def set_athena_result(
+        self,
+        rows: List[Dict[str, Any]],
+        column_info: List[Dict[str, str]],
+        account_id: str,
+        region: str,
+    ) -> None:
+        from moto.athena.models import athena_backends, QueryResults
+
+        backend = athena_backends[account_id][region]
+        results = QueryResults(rows=rows, column_info=column_info)
+        backend.query_results_queue.append(results)
+
 
 moto_api_backend = MotoAPIBackend(region_name="global", account_id=DEFAULT_ACCOUNT_ID)
diff --git a/moto/moto_api/_internal/responses.py b/moto/moto_api/_internal/responses.py
index ae52c6e47..03a0ac684 100644
--- a/moto/moto_api/_internal/responses.py
+++ b/moto/moto_api/_internal/responses.py
@@ -1,6 +1,7 @@
 import json
 
 from moto import settings
+from moto.core import DEFAULT_ACCOUNT_ID
 from moto.core.common_types import TYPE_RESPONSE
 from moto.core.responses import ActionAuthenticatorMixin, BaseResponse
 from typing import Any, Dict, List
@@ -141,3 +142,28 @@ class MotoAPIResponse(BaseResponse):
         a = self._get_param("a")
         mock_random.seed(int(a))
         return 200, {}, ""
+
+    def set_athena_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)
+        region = body.get("region", "us-east-1")
+
+        for result in body.get("results", []):
+            rows = result["rows"]
+            column_info = result.get("column_info", [])
+            moto_api_backend.set_athena_result(
+                rows=rows,
+                column_info=column_info,
+                account_id=account_id,
+                region=region,
+            )
+        return 201, {}, ""
diff --git a/moto/moto_api/_internal/urls.py b/moto/moto_api/_internal/urls.py
index da5ea6cef..0d1dd46a2 100644
--- a/moto/moto_api/_internal/urls.py
+++ b/moto/moto_api/_internal/urls.py
@@ -12,6 +12,7 @@ url_paths = {
     "{0}/moto-api/reset": response_instance.reset_response,
     "{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/state-manager/get-transition": response_instance.get_transition,
     "{0}/moto-api/state-manager/set-transition": response_instance.set_transition,
     "{0}/moto-api/state-manager/unset-transition": response_instance.unset_transition,
diff --git a/setup.cfg b/setup.cfg
index 15ba29675..9140bce3f 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -229,7 +229,7 @@ disable = W,C,R,E
 enable = anomalous-backslash-in-string, arguments-renamed, dangerous-default-value, deprecated-module, function-redefined, import-self, redefined-builtin, redefined-outer-name, reimported, pointless-statement, super-with-arguments, unused-argument, unused-import, unused-variable, useless-else-on-loop, wildcard-import
 
 [mypy]
-files= moto/a*,moto/b*,moto/c*,moto/d*,moto/ebs/,moto/ec2,moto/ec2instanceconnect,moto/ecr,moto/ecs,moto/efs,moto/eks,moto/elasticache,moto/es,moto/moto_api
+files= moto/a*,moto/b*,moto/c*,moto/d*,moto/ebs/,moto/ec2,moto/ec2instanceconnect,moto/ecr,moto/ecs,moto/efs,moto/eks,moto/elasticache,moto/elasticbeanstalk,moto/es,moto/moto_api
 show_column_numbers=True
 show_error_codes = True
 disable_error_code=abstract


Test patch:
diff --git a/tests/test_athena/test_athena.py b/tests/test_athena/test_athena.py
index 24ce64c96..57d4b1680 100644
--- a/tests/test_athena/test_athena.py
+++ b/tests/test_athena/test_athena.py
@@ -303,6 +303,8 @@ def test_get_query_results():
                 "CaseSensitive": True,
             }
         ]
+        # This was the documented way to configure query results, before `moto-api/static/athena/query_results` was implemented
+        # We should try to keep this for backward compatibility
         results = QueryResults(rows=rows, column_info=column_info)
         backend.query_results["test"] = results
 
@@ -311,6 +313,52 @@ def test_get_query_results():
         result["ResultSet"]["ResultSetMetadata"]["ColumnInfo"].should.equal(column_info)
 
 
+@mock_athena
+def test_get_query_results_queue():
+    client = boto3.client("athena", region_name="us-east-1")
+
+    result = client.get_query_results(QueryExecutionId="test")
+
+    result["ResultSet"]["Rows"].should.equal([])
+    result["ResultSet"]["ResultSetMetadata"]["ColumnInfo"].should.equal([])
+
+    if not settings.TEST_SERVER_MODE:
+        backend = athena_backends[DEFAULT_ACCOUNT_ID]["us-east-1"]
+        rows = [{"Data": [{"VarCharValue": ".."}]}]
+        column_info = [
+            {
+                "CatalogName": "string",
+                "SchemaName": "string",
+                "TableName": "string",
+                "Name": "string",
+                "Label": "string",
+                "Type": "string",
+                "Precision": 123,
+                "Scale": 123,
+                "Nullable": "NOT_NULL",
+                "CaseSensitive": True,
+            }
+        ]
+        results = QueryResults(rows=rows, column_info=column_info)
+        backend.query_results_queue.append(results)
+
+        result = client.get_query_results(
+            QueryExecutionId="some-id-not-used-when-results-were-added-to-queue"
+        )
+        result["ResultSet"]["Rows"].should.equal(rows)
+        result["ResultSet"]["ResultSetMetadata"]["ColumnInfo"].should.equal(column_info)
+
+        result = client.get_query_results(QueryExecutionId="other-id")
+        result["ResultSet"]["Rows"].should.equal([])
+        result["ResultSet"]["ResultSetMetadata"]["ColumnInfo"].should.equal([])
+
+        result = client.get_query_results(
+            QueryExecutionId="some-id-not-used-when-results-were-added-to-queue"
+        )
+        result["ResultSet"]["Rows"].should.equal(rows)
+        result["ResultSet"]["ResultSetMetadata"]["ColumnInfo"].should.equal(column_info)
+
+
 @mock_athena
 def test_list_query_executions():
     client = boto3.client("athena", region_name="us-east-1")
diff --git a/tests/test_athena/test_athena_server_api.py b/tests/test_athena/test_athena_server_api.py
new file mode 100644
index 000000000..2ebed930c
--- /dev/null
+++ b/tests/test_athena/test_athena_server_api.py
@@ -0,0 +1,161 @@
+import boto3
+import requests
+
+from moto import mock_athena, mock_sts, settings
+
+
+DEFAULT_COLUMN_INFO = [
+    {
+        "CatalogName": "string",
+        "SchemaName": "string",
+        "TableName": "string",
+        "Name": "string",
+        "Label": "string",
+        "Type": "string",
+        "Precision": 123,
+        "Scale": 123,
+        "Nullable": "NOT_NULL",
+        "CaseSensitive": True,
+    }
+]
+
+
+@mock_athena
+def test_set_athena_result():
+    base_url = (
+        "localhost:5000" if settings.TEST_SERVER_MODE else "motoapi.amazonaws.com"
+    )
+
+    athena_result = {
+        "results": [
+            {
+                "rows": [
+                    {"Data": [{"VarCharValue": "1"}]},
+                ],
+                "column_info": DEFAULT_COLUMN_INFO,
+            }
+        ]
+    }
+    resp = requests.post(
+        f"http://{base_url}/moto-api/static/athena/query-results",
+        json=athena_result,
+    )
+    resp.status_code.should.equal(201)
+
+    client = boto3.client("athena", region_name="us-east-1")
+    details = client.get_query_results(QueryExecutionId="anyid")["ResultSet"]
+    details["Rows"].should.equal(athena_result["results"][0]["rows"])
+    details["ResultSetMetadata"]["ColumnInfo"].should.equal(DEFAULT_COLUMN_INFO)
+
+    # Operation should be idempotent
+    details = client.get_query_results(QueryExecutionId="anyid")["ResultSet"]
+    details["Rows"].should.equal(athena_result["results"][0]["rows"])
+
+    # Different ID should still return different (default) results though
+    details = client.get_query_results(QueryExecutionId="otherid")["ResultSet"]
+    details["Rows"].should.equal([])
+
+
+@mock_athena
+def test_set_multiple_athena_result():
+    base_url = (
+        "localhost:5000" if settings.TEST_SERVER_MODE else "motoapi.amazonaws.com"
+    )
+
+    athena_result = {
+        "results": [
+            {"rows": [{"Data": [{"VarCharValue": "1"}]}]},
+            {"rows": [{"Data": [{"VarCharValue": "2"}]}]},
+            {"rows": [{"Data": [{"VarCharValue": "3"}]}]},
+        ]
+    }
+    resp = requests.post(
+        f"http://{base_url}/moto-api/static/athena/query-results",
+        json=athena_result,
+    )
+    resp.status_code.should.equal(201)
+
+    client = boto3.client("athena", region_name="us-east-1")
+    details = client.get_query_results(QueryExecutionId="first_id")["ResultSet"]
+    details["Rows"].should.equal([{"Data": [{"VarCharValue": "1"}]}])
+
+    # The same ID should return the same data
+    details = client.get_query_results(QueryExecutionId="first_id")["ResultSet"]
+    details["Rows"].should.equal([{"Data": [{"VarCharValue": "1"}]}])
+
+    # The next ID should return different data
+    details = client.get_query_results(QueryExecutionId="second_id")["ResultSet"]
+    details["Rows"].should.equal([{"Data": [{"VarCharValue": "2"}]}])
+
+    # The last ID should return even different data
+    details = client.get_query_results(QueryExecutionId="third_id")["ResultSet"]
+    details["Rows"].should.equal([{"Data": [{"VarCharValue": "3"}]}])
+
+    # Any other calls should return the default data
+    details = client.get_query_results(QueryExecutionId="other_id")["ResultSet"]
+    details["Rows"].should.equal([])
+
+
+@mock_athena
+@mock_sts
+def test_set_athena_result_with_custom_region_account():
+    base_url = (
+        "localhost:5000" if settings.TEST_SERVER_MODE else "motoapi.amazonaws.com"
+    )
+
+    athena_result = {
+        "account_id": "222233334444",
+        "region": "eu-west-1",
+        "results": [
+            {
+                "rows": [
+                    {"Data": [{"VarCharValue": "1"}]},
+                ],
+                "column_info": DEFAULT_COLUMN_INFO,
+            }
+        ],
+    }
+    resp = requests.post(
+        f"http://{base_url}/moto-api/static/athena/query-results",
+        json=athena_result,
+    )
+    resp.status_code.should.equal(201)
+
+    sts = boto3.client("sts", "us-east-1")
+    cross_account_creds = sts.assume_role(
+        RoleArn="arn:aws:iam::222233334444:role/role-in-another-account",
+        RoleSessionName="test-session-name",
+        ExternalId="test-external-id",
+    )["Credentials"]
+
+    athena_in_other_account = boto3.client(
+        "athena",
+        aws_access_key_id=cross_account_creds["AccessKeyId"],
+        aws_secret_access_key=cross_account_creds["SecretAccessKey"],
+        aws_session_token=cross_account_creds["SessionToken"],
+        region_name="eu-west-1",
+    )
+
+    details = athena_in_other_account.get_query_results(QueryExecutionId="anyid")[
+        "ResultSet"
+    ]
+    details["Rows"].should.equal(athena_result["results"][0]["rows"])
+    details["ResultSetMetadata"]["ColumnInfo"].should.equal(DEFAULT_COLUMN_INFO)
+
+    # query results from other regions do not match
+    athena_in_diff_region = boto3.client(
+        "athena",
+        aws_access_key_id=cross_account_creds["AccessKeyId"],
+        aws_secret_access_key=cross_account_creds["SecretAccessKey"],
+        aws_session_token=cross_account_creds["SessionToken"],
+        region_name="eu-west-2",
+    )
+    details = athena_in_diff_region.get_query_results(QueryExecutionId="anyid")[
+        "ResultSet"
+    ]
+    details["Rows"].should.equal([])
+
+    # query results from default account does not match
+    client = boto3.client("athena", region_name="eu-west-1")
+    details = client.get_query_results(QueryExecutionId="anyid")["ResultSet"]
+    details["Rows"].should.equal([])
diff --git a/tests/test_glue/test_datacatalog.py b/tests/test_glue/test_datacatalog.py
index 56956213a..da90df83c 100644
--- a/tests/test_glue/test_datacatalog.py
+++ b/tests/test_glue/test_datacatalog.py
@@ -384,6 +384,42 @@ def test_get_table_version_invalid_input():
     exc.value.response["Error"]["Code"].should.equal("InvalidInputException")
 
 
+@mock_glue
+def test_delete_table_version():
+    client = boto3.client("glue", region_name="us-east-1")
+    database_name = "myspecialdatabase"
+    helpers.create_database(client, database_name)
+
+    table_name = "myfirsttable"
+    version_inputs = {}
+
+    table_input = helpers.create_table_input(database_name, table_name)
+    helpers.create_table(client, database_name, table_name, table_input)
+    version_inputs["1"] = table_input
+
+    columns = [{"Name": "country", "Type": "string"}]
+    table_input = helpers.create_table_input(database_name, table_name, columns=columns)
+    helpers.update_table(client, database_name, table_name, table_input)
+    version_inputs["2"] = table_input
+
+    # Updateing with an identical input should still create a new version
+    helpers.update_table(client, database_name, table_name, table_input)
+    version_inputs["3"] = table_input
+
+    response = helpers.get_table_versions(client, database_name, table_name)
+    vers = response["TableVersions"]
+    vers.should.have.length_of(3)
+
+    client.delete_table_version(
+        DatabaseName=database_name, TableName=table_name, VersionId="2"
+    )
+
+    response = helpers.get_table_versions(client, database_name, table_name)
+    vers = response["TableVersions"]
+    vers.should.have.length_of(2)
+    [v["VersionId"] for v in vers].should.equal(["1", "3"])
+
+
 @mock_glue
 def test_get_table_not_exits():
     client = boto3.client("glue", region_name="us-east-1")
diff --git a/tests/test_kms/test_kms_encrypt.py b/tests/test_kms/test_kms_encrypt.py
index 6db34dcc3..a1b4c8aea 100644
--- a/tests/test_kms/test_kms_encrypt.py
+++ b/tests/test_kms/test_kms_encrypt.py
@@ -76,6 +76,24 @@ def test_encrypt_using_key_arn():
     kms.encrypt(KeyId=key_details["KeyMetadata"]["Arn"], Plaintext="hello")
 
 
+@mock_kms
+def test_re_encrypt_using_aliases():
+    client = boto3.client("kms", region_name="us-west-2")
+
+    key_1_id = client.create_key(Description="key 1")["KeyMetadata"]["KeyId"]
+    key_2_arn = client.create_key(Description="key 2")["KeyMetadata"]["Arn"]
+
+    key_alias = "alias/examplekey"
+    client.create_alias(AliasName=key_alias, TargetKeyId=key_2_arn)
+
+    encrypt_response = client.encrypt(KeyId=key_1_id, Plaintext="data")
+
+    client.re_encrypt(
+        CiphertextBlob=encrypt_response["CiphertextBlob"],
+        DestinationKeyId=key_alias,
+    )
+
+
 @pytest.mark.parametrize("plaintext", PLAINTEXT_VECTORS)
 @mock_kms
 def test_decrypt(plaintext):