Repository: getmoto/moto
Pull request #6264 (2023-04-27 15:46:52)

update CopyObject logic
Hello!
This PR should fix the issues mentioned in #6263

I tried to be as exhaustive as possible in the tests to allow reproduction of the current issues. I've also extensively tested those against AWS.

This would fix the following:
- Don't use the following from the source key:
  - `ServerSideEncryption`
  - `BucketKeyEnabled`
  - `SSEKMSKeyId`
  - `StorageClass`
  - `ACL`
  - `ObjectLockMode`
  - `ObjectLockRetainUntilDate`
  - `ObjectLockLegalHoldStatus`
- Use and set those parameters to the destination key:
  - `ObjectLockMode`
  - `ObjectLockRetainUntilDate`
  - `ObjectLockLegalHoldStatus`
- Rework the check to allow copying "in-place"
- Return `x-amz-copy-source-version-id` if the source bucket/key was versioned

If anything is missing, feel free and I'll try to fix it asap!

bblommers: Thanks for the PR @bentsku! I'm not quite sure why that particular test fails. It may be because of a recent werkzeug upgrade - you may have to merge in the latest master, I pushed a coule of fixes recently
bentsku: Oh I see my mistake now, I'm passing a `Bucket` resource to the client `Bucket` parameter... Will fix it quickly. 

Patch:
diff --git a/moto/cognitoidp/models.py b/moto/cognitoidp/models.py
index d0e19a2e3..b8cbb99bb 100644
--- a/moto/cognitoidp/models.py
+++ b/moto/cognitoidp/models.py
@@ -961,7 +961,7 @@ class CognitoIdpBackend(BaseBackend):
             "MfaConfiguration": user_pool.mfa_config,
         }
 
-    @paginate(pagination_model=PAGINATION_MODEL)
+    @paginate(pagination_model=PAGINATION_MODEL)  # type: ignore[misc]
     def list_user_pools(self) -> List[CognitoIdpUserPool]:  # type: ignore[misc]
         return list(self.user_pools.values())
 
@@ -1034,7 +1034,7 @@ class CognitoIdpBackend(BaseBackend):
         user_pool.clients[user_pool_client.id] = user_pool_client
         return user_pool_client
 
-    @paginate(pagination_model=PAGINATION_MODEL)
+    @paginate(pagination_model=PAGINATION_MODEL)  # type: ignore[misc]
     def list_user_pool_clients(self, user_pool_id: str) -> List[CognitoIdpUserPoolClient]:  # type: ignore[misc]
         user_pool = self.describe_user_pool(user_pool_id)
 
@@ -1081,7 +1081,7 @@ class CognitoIdpBackend(BaseBackend):
         user_pool.identity_providers[name] = identity_provider
         return identity_provider
 
-    @paginate(pagination_model=PAGINATION_MODEL)
+    @paginate(pagination_model=PAGINATION_MODEL)  # type: ignore[misc]
     def list_identity_providers(self, user_pool_id: str) -> List[CognitoIdpIdentityProvider]:  # type: ignore[misc]
         user_pool = self.describe_user_pool(user_pool_id)
 
@@ -1147,7 +1147,7 @@ class CognitoIdpBackend(BaseBackend):
 
         return user_pool.groups[group_name]
 
-    @paginate(pagination_model=PAGINATION_MODEL)
+    @paginate(pagination_model=PAGINATION_MODEL)  # type: ignore[misc]
     def list_groups(self, user_pool_id: str) -> List[CognitoIdpGroup]:  # type: ignore[misc]
         user_pool = self.describe_user_pool(user_pool_id)
 
@@ -1188,7 +1188,7 @@ class CognitoIdpBackend(BaseBackend):
         group.users.add(user)
         user.groups.add(group)
 
-    @paginate(pagination_model=PAGINATION_MODEL)
+    @paginate(pagination_model=PAGINATION_MODEL)  # type: ignore[misc]
     def list_users_in_group(self, user_pool_id: str, group_name: str) -> List[CognitoIdpUser]:  # type: ignore[misc]
         user_pool = self.describe_user_pool(user_pool_id)
         group = self.get_group(user_pool_id, group_name)
@@ -1324,7 +1324,7 @@ class CognitoIdpBackend(BaseBackend):
                 return user
         raise NotAuthorizedError("Invalid token")
 
-    @paginate(pagination_model=PAGINATION_MODEL)
+    @paginate(pagination_model=PAGINATION_MODEL)  # type: ignore[misc]
     def list_users(self, user_pool_id: str) -> List[CognitoIdpUser]:  # type: ignore[misc]
         user_pool = self.describe_user_pool(user_pool_id)
 
diff --git a/moto/databrew/models.py b/moto/databrew/models.py
index e4cd4a440..ba1d5e642 100644
--- a/moto/databrew/models.py
+++ b/moto/databrew/models.py
@@ -134,7 +134,7 @@ class DataBrewBackend(BaseBackend):
         recipe = self.recipes[recipe_name]
         recipe.update(recipe_description, recipe_steps)
 
-    @paginate(pagination_model=PAGINATION_MODEL)
+    @paginate(pagination_model=PAGINATION_MODEL)  # type: ignore[misc]
     def list_recipes(self, recipe_version: Optional[str] = None) -> List["FakeRecipeVersion"]:  # type: ignore[misc]
         # https://docs.aws.amazon.com/databrew/latest/dg/API_ListRecipes.html
         if recipe_version == FakeRecipe.LATEST_WORKING:
@@ -149,7 +149,7 @@ class DataBrewBackend(BaseBackend):
         recipes = [getattr(self.recipes[key], version) for key in self.recipes]
         return [r for r in recipes if r is not None]
 
-    @paginate(pagination_model=PAGINATION_MODEL)
+    @paginate(pagination_model=PAGINATION_MODEL)  # type: ignore[misc]
     def list_recipe_versions(self, recipe_name: str) -> List["FakeRecipeVersion"]:  # type: ignore[misc]
         # https://docs.aws.amazon.com/databrew/latest/dg/API_ListRecipeVersions.html
         self.validate_length(recipe_name, "name", 255)
@@ -253,7 +253,7 @@ class DataBrewBackend(BaseBackend):
             raise RulesetNotFoundException(ruleset_name)
         return self.rulesets[ruleset_name]
 
-    @paginate(pagination_model=PAGINATION_MODEL)
+    @paginate(pagination_model=PAGINATION_MODEL)  # type: ignore[misc]
     def list_rulesets(self) -> List["FakeRuleset"]:  # type: ignore[misc]
         return list(self.rulesets.values())
 
@@ -288,7 +288,7 @@ class DataBrewBackend(BaseBackend):
         self.datasets[dataset_name] = dataset
         return dataset
 
-    @paginate(pagination_model=PAGINATION_MODEL)
+    @paginate(pagination_model=PAGINATION_MODEL)  # type: ignore[misc]
     def list_datasets(self) -> List["FakeDataset"]:  # type: ignore[misc]
         return list(self.datasets.values())
 
@@ -405,7 +405,7 @@ class DataBrewBackend(BaseBackend):
         # https://docs.aws.amazon.com/databrew/latest/dg/API_UpdateProfileJob.html
         return self.update_job(**kwargs)
 
-    @paginate(pagination_model=PAGINATION_MODEL)
+    @paginate(pagination_model=PAGINATION_MODEL)  # type: ignore[misc]
     def list_jobs(self, dataset_name: Optional[str] = None, project_name: Optional[str] = None) -> List["FakeJob"]:  # type: ignore[misc]
         # https://docs.aws.amazon.com/databrew/latest/dg/API_ListJobs.html
         if dataset_name is not None:
diff --git a/moto/dax/models.py b/moto/dax/models.py
index 2a29194e5..f81387bd8 100644
--- a/moto/dax/models.py
+++ b/moto/dax/models.py
@@ -215,7 +215,7 @@ class DAXBackend(BaseBackend):
         self.clusters[cluster_name].delete()
         return self.clusters[cluster_name]
 
-    @paginate(PAGINATION_MODEL)
+    @paginate(PAGINATION_MODEL)  # type: ignore[misc]
     def describe_clusters(self, cluster_names: Iterable[str]) -> List[DaxCluster]:  # type: ignore[misc]
         clusters = self.clusters
         if not cluster_names:
diff --git a/moto/ds/models.py b/moto/ds/models.py
index 1d003f498..2dadfee01 100644
--- a/moto/ds/models.py
+++ b/moto/ds/models.py
@@ -476,7 +476,7 @@ class DirectoryServiceBackend(BaseBackend):
         directory = self.directories[directory_id]
         directory.enable_sso(True)
 
-    @paginate(pagination_model=PAGINATION_MODEL)
+    @paginate(pagination_model=PAGINATION_MODEL)  # type: ignore[misc]
     def describe_directories(self, directory_ids: Optional[List[str]] = None) -> List[Directory]:  # type: ignore[misc]
         """Return info on all directories or directories with matching IDs."""
         for directory_id in directory_ids or self.directories:
@@ -530,7 +530,7 @@ class DirectoryServiceBackend(BaseBackend):
         self._validate_directory_id(resource_id)
         self.tagger.untag_resource_using_names(resource_id, tag_keys)
 
-    @paginate(pagination_model=PAGINATION_MODEL)
+    @paginate(pagination_model=PAGINATION_MODEL)  # type: ignore[misc]
     def list_tags_for_resource(self, resource_id: str) -> List[Dict[str, str]]:  # type: ignore[misc]
         """List all tags on a directory."""
         self._validate_directory_id(resource_id)
diff --git a/moto/events/models.py b/moto/events/models.py
index f2908c22e..767d51864 100644
--- a/moto/events/models.py
+++ b/moto/events/models.py
@@ -1164,7 +1164,7 @@ class EventsBackend(BaseBackend):
 
         return False
 
-    @paginate(pagination_model=PAGINATION_MODEL)
+    @paginate(pagination_model=PAGINATION_MODEL)  # type: ignore[misc]
     def list_rule_names_by_target(self, target_arn: str, event_bus_arn: Optional[str]) -> List[Rule]:  # type: ignore[misc]
         event_bus_name = self._normalize_event_bus_arn(event_bus_arn)
         event_bus = self._get_event_bus(event_bus_name)
@@ -1177,7 +1177,7 @@ class EventsBackend(BaseBackend):
 
         return matching_rules
 
-    @paginate(pagination_model=PAGINATION_MODEL)
+    @paginate(pagination_model=PAGINATION_MODEL)  # type: ignore[misc]
     def list_rules(self, prefix: Optional[str] = None, event_bus_arn: Optional[str] = None) -> List[Rule]:  # type: ignore[misc]
         event_bus_name = self._normalize_event_bus_arn(event_bus_arn)
         event_bus = self._get_event_bus(event_bus_name)
diff --git a/moto/glue/models.py b/moto/glue/models.py
index 56e51a02a..6b288d650 100644
--- a/moto/glue/models.py
+++ b/moto/glue/models.py
@@ -320,7 +320,7 @@ class GlueBackend(BaseBackend):
     def get_crawlers(self) -> List["FakeCrawler"]:
         return [self.crawlers[key] for key in self.crawlers] if self.crawlers else []
 
-    @paginate(pagination_model=PAGINATION_MODEL)
+    @paginate(pagination_model=PAGINATION_MODEL)  # type: ignore[misc]
     def list_crawlers(self) -> List["FakeCrawler"]:  # type: ignore[misc]
         return [crawler for _, crawler in self.crawlers.items()]
 
@@ -395,7 +395,7 @@ class GlueBackend(BaseBackend):
         except KeyError:
             raise JobNotFoundException(name)
 
-    @paginate(pagination_model=PAGINATION_MODEL)
+    @paginate(pagination_model=PAGINATION_MODEL)  # type: ignore[misc]
     def get_jobs(self) -> List["FakeJob"]:  # type: ignore
         return [job for _, job in self.jobs.items()]
 
@@ -407,7 +407,7 @@ class GlueBackend(BaseBackend):
         job = self.get_job(name)
         return job.get_job_run(run_id)
 
-    @paginate(pagination_model=PAGINATION_MODEL)
+    @paginate(pagination_model=PAGINATION_MODEL)  # type: ignore[misc]
     def list_jobs(self) -> List["FakeJob"]:  # type: ignore
         return [job for _, job in self.jobs.items()]
 
@@ -812,7 +812,7 @@ class GlueBackend(BaseBackend):
         trigger = self.get_trigger(name)
         trigger.stop_trigger()
 
-    @paginate(pagination_model=PAGINATION_MODEL)
+    @paginate(pagination_model=PAGINATION_MODEL)  # type: ignore[misc]
     def get_triggers(self, dependent_job_name: str) -> List["FakeTrigger"]:  # type: ignore
         if dependent_job_name:
             triggers = []
@@ -826,7 +826,7 @@ class GlueBackend(BaseBackend):
 
         return list(self.triggers.values())
 
-    @paginate(pagination_model=PAGINATION_MODEL)
+    @paginate(pagination_model=PAGINATION_MODEL)  # type: ignore[misc]
     def list_triggers(self, dependent_job_name: str) -> List["FakeTrigger"]:  # type: ignore
         if dependent_job_name:
             triggers = []
diff --git a/moto/iot/models.py b/moto/iot/models.py
index 69715bf14..33f3664c6 100644
--- a/moto/iot/models.py
+++ b/moto/iot/models.py
@@ -1719,7 +1719,7 @@ class IoTBackend(BaseBackend):
 
         return job_executions, next_token
 
-    @paginate(PAGINATION_MODEL)
+    @paginate(PAGINATION_MODEL)  # type: ignore[misc]
     def list_job_executions_for_thing(self, thing_name: str, status: Optional[str]) -> List[Dict[str, Any]]:  # type: ignore[misc]
         job_executions = [
             self.job_executions[je].to_dict()
diff --git a/moto/kinesis/models.py b/moto/kinesis/models.py
index 0d6e7463b..171bad3e2 100644
--- a/moto/kinesis/models.py
+++ b/moto/kinesis/models.py
@@ -783,7 +783,7 @@ class KinesisBackend(BaseBackend):
 
         return current_shard_count
 
-    @paginate(pagination_model=PAGINATION_MODEL)
+    @paginate(pagination_model=PAGINATION_MODEL)  # type: ignore[misc]
     def list_shards(self, stream_arn: Optional[str], stream_name: Optional[str]) -> List[Dict[str, Any]]:  # type: ignore
         stream = self.describe_stream(stream_arn=stream_arn, stream_name=stream_name)
         shards = sorted(stream.shards.values(), key=lambda x: x.shard_id)
diff --git a/moto/logs/models.py b/moto/logs/models.py
index 09ed64f1a..f4a76433e 100644
--- a/moto/logs/models.py
+++ b/moto/logs/models.py
@@ -695,7 +695,7 @@ class LogsBackend(BaseBackend):
             raise ResourceNotFoundException()
         del self.groups[log_group_name]
 
-    @paginate(pagination_model=PAGINATION_MODEL)
+    @paginate(pagination_model=PAGINATION_MODEL)  # type: ignore[misc]
     def describe_log_groups(self, log_group_name_prefix: Optional[str] = None) -> List[Dict[str, Any]]:  # type: ignore[misc]
         groups = [
             group.to_describe_dict()
diff --git a/moto/organizations/models.py b/moto/organizations/models.py
index d40230184..80362a3d6 100644
--- a/moto/organizations/models.py
+++ b/moto/organizations/models.py
@@ -441,7 +441,7 @@ class OrganizationsBackend(BaseBackend):
         ou = self.get_organizational_unit_by_id(kwargs["OrganizationalUnitId"])
         return ou.describe()
 
-    @paginate(pagination_model=PAGINATION_MODEL)
+    @paginate(pagination_model=PAGINATION_MODEL)  # type: ignore[misc]
     def list_organizational_units_for_parent(self, **kwargs: Any) -> List[Dict[str, Any]]:  # type: ignore
         parent_id = self.validate_parent_id(kwargs["parent_id"])
         return [
@@ -515,12 +515,12 @@ class OrganizationsBackend(BaseBackend):
             next_token = str(len(accounts_resp))
         return dict(CreateAccountStatuses=accounts_resp, NextToken=next_token)
 
-    @paginate(pagination_model=PAGINATION_MODEL)
+    @paginate(pagination_model=PAGINATION_MODEL)  # type: ignore[misc]
     def list_accounts(self) -> List[FakeAccount]:  # type: ignore
         accounts = [account.describe() for account in self.accounts]
         return sorted(accounts, key=lambda x: x["JoinedTimestamp"])  # type: ignore
 
-    @paginate(pagination_model=PAGINATION_MODEL)
+    @paginate(pagination_model=PAGINATION_MODEL)  # type: ignore[misc]
     def list_accounts_for_parent(self, **kwargs: Any) -> Any:  # type: ignore
         parent_id = self.validate_parent_id(kwargs["parent_id"])
         accounts = [
diff --git a/moto/route53/models.py b/moto/route53/models.py
index 2e6cc8de2..81f9cb77d 100644
--- a/moto/route53/models.py
+++ b/moto/route53/models.py
@@ -877,7 +877,7 @@ class Route53Backend(BaseBackend):
             raise NoSuchQueryLoggingConfig()
         return self.query_logging_configs[query_logging_config_id]
 
-    @paginate(pagination_model=PAGINATION_MODEL)
+    @paginate(pagination_model=PAGINATION_MODEL)  # type: ignore[misc]
     def list_query_logging_configs(self, hosted_zone_id: Optional[str] = None) -> List[QueryLoggingConfig]:  # type: ignore
         """Return a list of query logging configs."""
         if hosted_zone_id:
diff --git a/moto/route53resolver/models.py b/moto/route53resolver/models.py
index 2deee1383..8c3fed139 100644
--- a/moto/route53resolver/models.py
+++ b/moto/route53resolver/models.py
@@ -754,7 +754,7 @@ class Route53ResolverBackend(BaseBackend):
             )
         return self.resolver_rule_associations[resolver_rule_association_id]
 
-    @paginate(pagination_model=PAGINATION_MODEL)
+    @paginate(pagination_model=PAGINATION_MODEL)  # type: ignore[misc]
     def list_resolver_endpoint_ip_addresses(self, resolver_endpoint_id: str) -> List[Dict[str, Any]]:  # type: ignore[misc]
         self._validate_resolver_endpoint_id(resolver_endpoint_id)
         endpoint = self.resolver_endpoints[resolver_endpoint_id]
@@ -813,7 +813,7 @@ class Route53ResolverBackend(BaseBackend):
 
         return True
 
-    @paginate(pagination_model=PAGINATION_MODEL)
+    @paginate(pagination_model=PAGINATION_MODEL)  # type: ignore[misc]
     def list_resolver_endpoints(self, filters: Any) -> List[ResolverEndpoint]:  # type: ignore[misc]
         if not filters:
             filters = []
@@ -827,7 +827,7 @@ class Route53ResolverBackend(BaseBackend):
                 endpoints.append(endpoint)
         return endpoints
 
-    @paginate(pagination_model=PAGINATION_MODEL)
+    @paginate(pagination_model=PAGINATION_MODEL)  # type: ignore[misc]
     def list_resolver_rules(self, filters: Any) -> List[ResolverRule]:  # type: ignore[misc]
         if not filters:
             filters = []
@@ -841,7 +841,7 @@ class Route53ResolverBackend(BaseBackend):
                 rules.append(rule)
         return rules
 
-    @paginate(pagination_model=PAGINATION_MODEL)
+    @paginate(pagination_model=PAGINATION_MODEL)  # type: ignore[misc]
     def list_resolver_rule_associations(self, filters: Any) -> List[ResolverRuleAssociation]:  # type: ignore[misc]
         if not filters:
             filters = []
@@ -869,7 +869,7 @@ class Route53ResolverBackend(BaseBackend):
             f"Resolver endpoint with ID '{resource_arn}' does not exist"
         )
 
-    @paginate(pagination_model=PAGINATION_MODEL)
+    @paginate(pagination_model=PAGINATION_MODEL)  # type: ignore[misc]
     def list_tags_for_resource(self, resource_arn: str) -> Optional[List[Dict[str, str]]]:  # type: ignore[misc]
         self._matched_arn(resource_arn)
         return self.tagger.list_tags_for_resource(resource_arn).get("Tags")
diff --git a/moto/s3/models.py b/moto/s3/models.py
index fa6fe080a..e6365dabf 100644
--- a/moto/s3/models.py
+++ b/moto/s3/models.py
@@ -119,7 +119,7 @@ class FakeKey(BaseModel, ManagedState):
         self.account_id = account_id
         self.last_modified = datetime.datetime.utcnow()
         self.acl: Optional[FakeAcl] = get_canned_acl("private")
-        self.website_redirect_location = None
+        self.website_redirect_location: Optional[str] = None
         self.checksum_algorithm = None
         self._storage_class: Optional[str] = storage if storage else "STANDARD"
         self._metadata = LowercaseDict()
@@ -2359,42 +2359,55 @@ class S3Backend(BaseBackend, CloudWatchMetricProvider):
         dest_bucket_name: str,
         dest_key_name: str,
         storage: Optional[str] = None,
-        acl: Optional[FakeAcl] = None,
         encryption: Optional[str] = None,
         kms_key_id: Optional[str] = None,
-        bucket_key_enabled: bool = False,
+        bucket_key_enabled: Any = None,
         mdirective: Optional[str] = None,
+        metadata: Optional[Any] = None,
+        website_redirect_location: Optional[str] = None,
+        lock_mode: Optional[str] = None,
+        lock_legal_status: Optional[str] = None,
+        lock_until: Optional[str] = None,
     ) -> None:
-        if (
-            src_key.name == dest_key_name
-            and src_key.bucket_name == dest_bucket_name
-            and storage == src_key.storage_class
-            and acl == src_key.acl
-            and encryption == src_key.encryption
-            and kms_key_id == src_key.kms_key_id
-            and bucket_key_enabled == (src_key.bucket_key_enabled or False)
-            and mdirective != "REPLACE"
-        ):
-            raise CopyObjectMustChangeSomething
+        if src_key.name == dest_key_name and src_key.bucket_name == dest_bucket_name:
+            if src_key.encryption and src_key.encryption != "AES256" and not encryption:
+                # this a special case, as now S3 default to AES256 when not provided
+                # if the source key had encryption, and we did not specify it for the destination, S3 will accept a
+                # copy in place even without any required attributes
+                encryption = "AES256"
+
+            if not any(
+                (
+                    storage,
+                    encryption,
+                    mdirective == "REPLACE",
+                    website_redirect_location,
+                )
+            ):
+                raise CopyObjectMustChangeSomething
 
         new_key = self.put_object(
             bucket_name=dest_bucket_name,
             key_name=dest_key_name,
             value=src_key.value,
-            storage=storage or src_key.storage_class,
+            storage=storage,
             multipart=src_key.multipart,
-            encryption=encryption or src_key.encryption,
-            kms_key_id=kms_key_id or src_key.kms_key_id,
-            bucket_key_enabled=bucket_key_enabled or src_key.bucket_key_enabled,
-            lock_mode=src_key.lock_mode,
-            lock_legal_status=src_key.lock_legal_status,
-            lock_until=src_key.lock_until,
+            encryption=encryption,
+            kms_key_id=kms_key_id,  # TODO: use aws managed key if not provided
+            bucket_key_enabled=bucket_key_enabled,
+            lock_mode=lock_mode,
+            lock_legal_status=lock_legal_status,
+            lock_until=lock_until,
         )
         self.tagger.copy_tags(src_key.arn, new_key.arn)
-        new_key.set_metadata(src_key.metadata)
+        if mdirective != "REPLACE":
+            new_key.set_metadata(src_key.metadata)
+        else:
+            new_key.set_metadata(metadata)
+
+        if website_redirect_location:
+            new_key.website_redirect_location = website_redirect_location
 
-        if acl is not None:
-            new_key.set_acl(acl)
         if src_key.storage_class in ARCHIVE_STORAGE_CLASSES:
             # Object copied from Glacier object should not have expiry
             new_key.set_expiry(None)
diff --git a/moto/s3/responses.py b/moto/s3/responses.py
index fc5ce8306..10270de8f 100644
--- a/moto/s3/responses.py
+++ b/moto/s3/responses.py
@@ -1586,37 +1586,43 @@ class S3Response(BaseResponse):
                     ):
                         raise ObjectNotInActiveTierError(key_to_copy)
 
-                bucket_key_enabled = (
-                    request.headers.get(
-                        "x-amz-server-side-encryption-bucket-key-enabled", ""
-                    ).lower()
-                    == "true"
+                website_redirect_location = request.headers.get(
+                    "x-amz-website-redirect-location"
                 )
 
                 mdirective = request.headers.get("x-amz-metadata-directive")
-
+                metadata = metadata_from_headers(request.headers)
                 self.backend.copy_object(
                     key_to_copy,
                     bucket_name,
                     key_name,
-                    storage=storage_class,
-                    acl=acl,
+                    storage=request.headers.get("x-amz-storage-class"),
                     kms_key_id=kms_key_id,
                     encryption=encryption,
                     bucket_key_enabled=bucket_key_enabled,
                     mdirective=mdirective,
+                    metadata=metadata,
+                    website_redirect_location=website_redirect_location,
+                    lock_mode=lock_mode,
+                    lock_legal_status=legal_hold,
+                    lock_until=lock_until,
                 )
             else:
                 raise MissingKey(key=src_key)
 
             new_key: FakeKey = self.backend.get_object(bucket_name, key_name)  # type: ignore
-            if mdirective is not None and mdirective == "REPLACE":
-                metadata = metadata_from_headers(request.headers)
-                new_key.set_metadata(metadata, replace=True)
+
+            if acl is not None:
+                new_key.set_acl(acl)
+
             tdirective = request.headers.get("x-amz-tagging-directive")
             if tdirective == "REPLACE":
                 tagging = self._tagging_from_headers(request.headers)
                 self.backend.set_key_tags(new_key, tagging)
+            if key_to_copy.version_id != "null":
+                response_headers[
+                    "x-amz-copy-source-version-id"
+                ] = key_to_copy.version_id
 
             # checksum stuff, do we need to compute hash of the copied object
             checksum_algorithm = request.headers.get("x-amz-checksum-algorithm")
diff --git a/moto/sagemaker/models.py b/moto/sagemaker/models.py
index cab49ff18..74cf00657 100644
--- a/moto/sagemaker/models.py
+++ b/moto/sagemaker/models.py
@@ -1343,7 +1343,7 @@ class SageMakerModelBackend(BaseBackend):
         resource.tags.extend(tags)
         return resource.tags
 
-    @paginate(pagination_model=PAGINATION_MODEL)
+    @paginate(pagination_model=PAGINATION_MODEL)  # type: ignore[misc]
     def list_tags(self, arn: str) -> List[Dict[str, str]]:  # type: ignore[misc]
         resource = self._get_resource_from_arn(arn)
         return resource.tags
@@ -1352,7 +1352,7 @@ class SageMakerModelBackend(BaseBackend):
         resource = self._get_resource_from_arn(arn)
         resource.tags = [tag for tag in resource.tags if tag["Key"] not in tag_keys]
 
-    @paginate(pagination_model=PAGINATION_MODEL)
+    @paginate(pagination_model=PAGINATION_MODEL)  # type: ignore[misc]
     def list_experiments(self) -> List["FakeExperiment"]:  # type: ignore[misc]
         return list(self.experiments.values())
 
@@ -1522,7 +1522,7 @@ class SageMakerModelBackend(BaseBackend):
                 message=f"Could not find trial configuration '{arn}'."
             )
 
-    @paginate(pagination_model=PAGINATION_MODEL)
+    @paginate(pagination_model=PAGINATION_MODEL)  # type: ignore[misc]
     def list_trials(self, experiment_name: Optional[str] = None, trial_component_name: Optional[str] = None) -> List["FakeTrial"]:  # type: ignore[misc]
         trials_fetched = list(self.trials.values())
 
@@ -1581,7 +1581,7 @@ class SageMakerModelBackend(BaseBackend):
     ) -> None:
         self.trial_components[trial_component_name].update(details_json)
 
-    @paginate(pagination_model=PAGINATION_MODEL)
+    @paginate(pagination_model=PAGINATION_MODEL)  # type: ignore[misc]
     def list_trial_components(self, trial_name: Optional[str] = None) -> List["FakeTrialComponent"]:  # type: ignore[misc]
         trial_components_fetched = list(self.trial_components.values())
 
diff --git a/moto/scheduler/responses.py b/moto/scheduler/responses.py
index 9517840e6..ae0e9ef89 100644
--- a/moto/scheduler/responses.py
+++ b/moto/scheduler/responses.py
@@ -60,7 +60,7 @@ class EventBridgeSchedulerResponse(BaseResponse):
         return "{}"
 
     def update_schedule(self) -> str:
-        group_name = self._get_param("groupName")
+        group_name = self._get_param("GroupName")
         name = self.uri.split("?")[0].split("/")[-1]
         description = self._get_param("Description")
         end_date = self._get_param("EndDate")
diff --git a/moto/ssoadmin/models.py b/moto/ssoadmin/models.py
index d83c96147..a132ef452 100644
--- a/moto/ssoadmin/models.py
+++ b/moto/ssoadmin/models.py
@@ -249,7 +249,7 @@ class SSOAdminBackend(BaseBackend):
                 return permission_set
         raise ResourceNotFound
 
-    @paginate(pagination_model=PAGINATION_MODEL)
+    @paginate(pagination_model=PAGINATION_MODEL)  # type: ignore[misc]
     def list_permission_sets(self, instance_arn: str) -> List[PermissionSet]:  # type: ignore[misc]
         permission_sets = []
         for permission_set in self.permission_sets:
diff --git a/moto/stepfunctions/models.py b/moto/stepfunctions/models.py
index 5628c95dd..3df09e648 100644
--- a/moto/stepfunctions/models.py
+++ b/moto/stepfunctions/models.py
@@ -500,7 +500,7 @@ class StepFunctionBackend(BaseBackend):
             self.state_machines.append(state_machine)
             return state_machine
 
-    @paginate(pagination_model=PAGINATION_MODEL)
+    @paginate(pagination_model=PAGINATION_MODEL)  # type: ignore[misc]
     def list_state_machines(self) -> Iterable[StateMachine]:  # type: ignore[misc]
         return sorted(self.state_machines, key=lambda x: x.creation_date)
 
@@ -546,7 +546,7 @@ class StepFunctionBackend(BaseBackend):
         state_machine = self._get_state_machine_for_execution(execution_arn)
         return state_machine.stop_execution(execution_arn)
 
-    @paginate(pagination_model=PAGINATION_MODEL)
+    @paginate(pagination_model=PAGINATION_MODEL)  # type: ignore[misc]
     def list_executions(self, state_machine_arn: str, status_filter: Optional[str] = None) -> Iterable[Execution]:  # type: ignore[misc]
         """
         The status of every execution is set to 'RUNNING' by default.
diff --git a/moto/textract/exceptions.py b/moto/textract/exceptions.py
index 9c0e512fc..3391df15e 100644
--- a/moto/textract/exceptions.py
+++ b/moto/textract/exceptions.py
@@ -5,16 +5,16 @@ from moto.core.exceptions import JsonRESTError
 class InvalidJobIdException(JsonRESTError):
     code = 400
 
-    def __init__(self):
-        super().__init__(__class__.__name__, "An invalid job identifier was passed.")
+    def __init__(self) -> None:
+        super().__init__(__class__.__name__, "An invalid job identifier was passed.")  # type: ignore
 
 
 class InvalidS3ObjectException(JsonRESTError):
     code = 400
 
-    def __init__(self):
+    def __init__(self) -> None:
         super().__init__(
-            __class__.__name__,
+            __class__.__name__,  # type: ignore
             "Amazon Textract is unable to access the S3 object that's specified in the request.",
         )
 
@@ -22,8 +22,8 @@ class InvalidS3ObjectException(JsonRESTError):
 class InvalidParameterException(JsonRESTError):
     code = 400
 
-    def __init__(self):
+    def __init__(self) -> None:
         super().__init__(
-            __class__.__name__,
+            __class__.__name__,  # type: ignore
             "An input parameter violated a constraint. For example, in synchronous operations, an InvalidParameterException exception occurs when neither of the S3Object or Bytes values are supplied in the Document request parameter. Validate your parameter before calling the API operation again.",
         )
diff --git a/moto/textract/models.py b/moto/textract/models.py
index a7526ad16..876acc2f5 100644
--- a/moto/textract/models.py
+++ b/moto/textract/models.py
@@ -1,6 +1,5 @@
-"""TextractBackend class with methods for supported APIs."""
-
 from collections import defaultdict
+from typing import Any, Dict, List
 
 from moto.core import BaseBackend, BackendDict, BaseModel
 from moto.moto_api._internal import mock_random
@@ -16,10 +15,10 @@ class TextractJobStatus:
 
 
 class TextractJob(BaseModel):
-    def __init__(self, job):
+    def __init__(self, job: Dict[str, Any]):
         self.job = job
 
-    def to_dict(self):
+    def to_dict(self) -> Dict[str, Any]:
         return self.job
 
 
@@ -28,13 +27,13 @@ class TextractBackend(BaseBackend):
 
     JOB_STATUS = TextractJobStatus.succeeded
     PAGES = {"Pages": mock_random.randint(5, 500)}
-    BLOCKS = []
+    BLOCKS: List[Dict[str, Any]] = []
 
-    def __init__(self, region_name, account_id):
+    def __init__(self, region_name: str, account_id: str):
         super().__init__(region_name, account_id)
-        self.async_text_detection_jobs = defaultdict()
+        self.async_text_detection_jobs: Dict[str, TextractJob] = defaultdict()
 
-    def get_document_text_detection(self, job_id):
+    def get_document_text_detection(self, job_id: str) -> TextractJob:
         """
         Pagination has not yet been implemented
         """
@@ -43,7 +42,7 @@ class TextractBackend(BaseBackend):
             raise InvalidJobIdException()
         return job
 
-    def start_document_text_detection(self, document_location):
+    def start_document_text_detection(self, document_location: str) -> str:
         """
         The following parameters have not yet been implemented: ClientRequestToken, JobTag, NotificationChannel, OutputConfig, KmsKeyID
         """
diff --git a/moto/textract/responses.py b/moto/textract/responses.py
index b10949531..df72a5ded 100644
--- a/moto/textract/responses.py
+++ b/moto/textract/responses.py
@@ -2,27 +2,27 @@
 import json
 
 from moto.core.responses import BaseResponse
-from .models import textract_backends
+from .models import textract_backends, TextractBackend
 
 
 class TextractResponse(BaseResponse):
     """Handler for Textract requests and responses."""
 
-    def __init__(self):
+    def __init__(self) -> None:
         super().__init__(service_name="textract")
 
     @property
-    def textract_backend(self):
+    def textract_backend(self) -> TextractBackend:
         """Return backend instance specific for this region."""
         return textract_backends[self.current_account][self.region]
 
-    def get_document_text_detection(self):
+    def get_document_text_detection(self) -> str:
         params = json.loads(self.body)
         job_id = params.get("JobId")
         job = self.textract_backend.get_document_text_detection(job_id=job_id).to_dict()
         return json.dumps(job)
 
-    def start_document_text_detection(self):
+    def start_document_text_detection(self) -> str:
         params = json.loads(self.body)
         document_location = params.get("DocumentLocation")
         job_id = self.textract_backend.start_document_text_detection(
diff --git a/moto/timestreamwrite/exceptions.py b/moto/timestreamwrite/exceptions.py
index f1598bd65..18efc08d5 100644
--- a/moto/timestreamwrite/exceptions.py
+++ b/moto/timestreamwrite/exceptions.py
@@ -5,5 +5,5 @@ from moto.core.exceptions import JsonRESTError
 class ResourceNotFound(JsonRESTError):
     error_type = "com.amazonaws.timestream.v20181101#ResourceNotFoundException"
 
-    def __init__(self, msg):
+    def __init__(self, msg: str):
         super().__init__(ResourceNotFound.error_type, msg)
diff --git a/moto/timestreamwrite/models.py b/moto/timestreamwrite/models.py
index c14b8b46e..4aae3226c 100644
--- a/moto/timestreamwrite/models.py
+++ b/moto/timestreamwrite/models.py
@@ -1,3 +1,4 @@
+from typing import Any, Dict, List, Iterable
 from moto.core import BaseBackend, BackendDict, BaseModel
 from moto.utilities.tagging_service import TaggingService
 from .exceptions import ResourceNotFound
@@ -6,12 +7,12 @@ from .exceptions import ResourceNotFound
 class TimestreamTable(BaseModel):
     def __init__(
         self,
-        account_id,
-        region_name,
-        table_name,
-        db_name,
-        retention_properties,
-        magnetic_store_write_properties,
+        account_id: str,
+        region_name: str,
+        table_name: str,
+        db_name: str,
+        retention_properties: Dict[str, int],
+        magnetic_store_write_properties: Dict[str, Any],
     ):
         self.region_name = region_name
         self.name = table_name
@@ -21,18 +22,22 @@ class TimestreamTable(BaseModel):
             "MagneticStoreRetentionPeriodInDays": 123,
         }
         self.magnetic_store_write_properties = magnetic_store_write_properties or {}
-        self.records = []
+        self.records: List[Dict[str, Any]] = []
         self.arn = f"arn:aws:timestream:{self.region_name}:{account_id}:database/{self.db_name}/table/{self.name}"
 
-    def update(self, retention_properties, magnetic_store_write_properties):
+    def update(
+        self,
+        retention_properties: Dict[str, int],
+        magnetic_store_write_properties: Dict[str, Any],
+    ) -> None:
         self.retention_properties = retention_properties
         if magnetic_store_write_properties is not None:
             self.magnetic_store_write_properties = magnetic_store_write_properties
 
-    def write_records(self, records):
+    def write_records(self, records: List[Dict[str, Any]]) -> None:
         self.records.extend(records)
 
-    def description(self):
+    def description(self) -> Dict[str, Any]:
         return {
             "Arn": self.arn,
             "TableName": self.name,
@@ -44,7 +49,9 @@ class TimestreamTable(BaseModel):
 
 
 class TimestreamDatabase(BaseModel):
-    def __init__(self, account_id, region_name, database_name, kms_key_id):
+    def __init__(
+        self, account_id: str, region_name: str, database_name: str, kms_key_id: str
+    ):
         self.account_id = account_id
         self.region_name = region_name
         self.name = database_name
@@ -54,14 +61,17 @@ class TimestreamDatabase(BaseModel):
         self.arn = (
             f"arn:aws:timestream:{self.region_name}:{account_id}:database/{self.name}"
         )
-        self.tables = dict()
+        self.tables: Dict[str, TimestreamTable] = dict()
 
-    def update(self, kms_key_id):
+    def update(self, kms_key_id: str) -> None:
         self.kms_key_id = kms_key_id
 
     def create_table(
-        self, table_name, retention_properties, magnetic_store_write_properties
-    ):
+        self,
+        table_name: str,
+        retention_properties: Dict[str, int],
+        magnetic_store_write_properties: Dict[str, Any],
+    ) -> TimestreamTable:
         table = TimestreamTable(
             account_id=self.account_id,
             region_name=self.region_name,
@@ -74,8 +84,11 @@ class TimestreamDatabase(BaseModel):
         return table
 
     def update_table(
-        self, table_name, retention_properties, magnetic_store_write_properties
-    ):
+        self,
+        table_name: str,
+        retention_properties: Dict[str, int],
+        magnetic_store_write_properties: Dict[str, Any],
+    ) -> TimestreamTable:
         table = self.tables[table_name]
         table.update(
             retention_properties=retention_properties,
@@ -83,18 +96,18 @@ class TimestreamDatabase(BaseModel):
         )
         return table
 
-    def delete_table(self, table_name):
+    def delete_table(self, table_name: str) -> None:
         self.tables.pop(table_name, None)
 
-    def describe_table(self, table_name):
+    def describe_table(self, table_name: str) -> TimestreamTable:
         if table_name not in self.tables:
             raise ResourceNotFound(f"The table {table_name} does not exist.")
         return self.tables[table_name]
 
-    def list_tables(self):
+    def list_tables(self) -> Iterable[TimestreamTable]:
         return self.tables.values()
 
-    def description(self):
+    def description(self) -> Dict[str, Any]:
         return {
             "Arn": self.arn,
             "DatabaseName": self.name,
@@ -117,12 +130,14 @@ class TimestreamWriteBackend(BaseBackend):
 
     """
 
-    def __init__(self, region_name, account_id):
+    def __init__(self, region_name: str, account_id: str):
         super().__init__(region_name, account_id)
-        self.databases = dict()
+        self.databases: Dict[str, TimestreamDatabase] = dict()
         self.tagging_service = TaggingService()
 
-    def create_database(self, database_name, kms_key_id, tags):
+    def create_database(
+        self, database_name: str, kms_key_id: str, tags: List[Dict[str, str]]
+    ) -> TimestreamDatabase:
         database = TimestreamDatabase(
             self.account_id, self.region_name, database_name, kms_key_id
         )
@@ -130,30 +145,32 @@ class TimestreamWriteBackend(BaseBackend):
         self.tagging_service.tag_resource(database.arn, tags)
         return database
 
-    def delete_database(self, database_name):
+    def delete_database(self, database_name: str) -> None:
         del self.databases[database_name]
 
-    def describe_database(self, database_name):
+    def describe_database(self, database_name: str) -> TimestreamDatabase:
         if database_name not in self.databases:
             raise ResourceNotFound(f"The database {database_name} does not exist.")
         return self.databases[database_name]
 
-    def list_databases(self):
+    def list_databases(self) -> Iterable[TimestreamDatabase]:
         return self.databases.values()
 
-    def update_database(self, database_name, kms_key_id):
+    def update_database(
+        self, database_name: str, kms_key_id: str
+    ) -> TimestreamDatabase:
         database = self.databases[database_name]
         database.update(kms_key_id=kms_key_id)
         return database
 
     def create_table(
         self,
-        database_name,
-        table_name,
-        retention_properties,
-        tags,
-        magnetic_store_write_properties,
-    ):
+        database_name: str,
+        table_name: str,
+        retention_properties: Dict[str, int],
+        tags: List[Dict[str, str]],
+        magnetic_store_write_properties: Dict[str, Any],
+    ) -> TimestreamTable:
         database = self.describe_database(database_name)
         table = database.create_table(
             table_name, retention_properties, magnetic_store_write_properties
@@ -161,39 +178,38 @@ class TimestreamWriteBackend(BaseBackend):
         self.tagging_service.tag_resource(table.arn, tags)
         return table
 
-    def delete_table(self, database_name, table_name):
+    def delete_table(self, database_name: str, table_name: str) -> None:
         database = self.describe_database(database_name)
         database.delete_table(table_name)
 
-    def describe_table(self, database_name, table_name):
+    def describe_table(self, database_name: str, table_name: str) -> TimestreamTable:
         database = self.describe_database(database_name)
-        table = database.describe_table(table_name)
-        return table
+        return database.describe_table(table_name)
 
-    def list_tables(self, database_name):
+    def list_tables(self, database_name: str) -> Iterable[TimestreamTable]:
         database = self.describe_database(database_name)
-        tables = database.list_tables()
-        return tables
+        return database.list_tables()
 
     def update_table(
         self,
-        database_name,
-        table_name,
-        retention_properties,
-        magnetic_store_write_properties,
-    ):
+        database_name: str,
+        table_name: str,
+        retention_properties: Dict[str, int],
+        magnetic_store_write_properties: Dict[str, Any],
+    ) -> TimestreamTable:
         database = self.describe_database(database_name)
-        table = database.update_table(
+        return database.update_table(
             table_name, retention_properties, magnetic_store_write_properties
         )
-        return table
 
-    def write_records(self, database_name, table_name, records):
+    def write_records(
+        self, database_name: str, table_name: str, records: List[Dict[str, Any]]
+    ) -> None:
         database = self.describe_database(database_name)
         table = database.describe_table(table_name)
         table.write_records(records)
 
-    def describe_endpoints(self):
+    def describe_endpoints(self) -> Dict[str, List[Dict[str, Any]]]:
         # https://docs.aws.amazon.com/timestream/latest/developerguide/Using-API.endpoint-discovery.how-it-works.html
         # Usually, the address look like this:
         # ingest-cell1.timestream.us-east-1.amazonaws.com
@@ -208,13 +224,15 @@ class TimestreamWriteBackend(BaseBackend):
             ]
         }
 
-    def list_tags_for_resource(self, resource_arn):
+    def list_tags_for_resource(
+        self, resource_arn: str
+    ) -> Dict[str, List[Dict[str, str]]]:
         return self.tagging_service.list_tags_for_resource(resource_arn)
 
-    def tag_resource(self, resource_arn, tags):
+    def tag_resource(self, resource_arn: str, tags: List[Dict[str, str]]) -> None:
         self.tagging_service.tag_resource(resource_arn, tags)
 
-    def untag_resource(self, resource_arn, tag_keys):
+    def untag_resource(self, resource_arn: str, tag_keys: List[str]) -> None:
         self.tagging_service.untag_resource_using_names(resource_arn, tag_keys)
 
 
diff --git a/moto/timestreamwrite/responses.py b/moto/timestreamwrite/responses.py
index 2580b3a3b..cfb8e4829 100644
--- a/moto/timestreamwrite/responses.py
+++ b/moto/timestreamwrite/responses.py
@@ -1,19 +1,19 @@
 import json
 
 from moto.core.responses import BaseResponse
-from .models import timestreamwrite_backends
+from .models import timestreamwrite_backends, TimestreamWriteBackend
 
 
 class TimestreamWriteResponse(BaseResponse):
-    def __init__(self):
+    def __init__(self) -> None:
         super().__init__(service_name="timestream-write")
 
     @property
-    def timestreamwrite_backend(self):
+    def timestreamwrite_backend(self) -> TimestreamWriteBackend:
         """Return backend instance specific for this region."""
         return timestreamwrite_backends[self.current_account][self.region]
 
-    def create_database(self):
+    def create_database(self) -> str:
         database_name = self._get_param("DatabaseName")
         kms_key_id = self._get_param("KmsKeyId")
         tags = self._get_param("Tags")
@@ -22,19 +22,19 @@ class TimestreamWriteResponse(BaseResponse):
         )
         return json.dumps(dict(Database=database.description()))
 
-    def delete_database(self):
+    def delete_database(self) -> str:
         database_name = self._get_param("DatabaseName")
         self.timestreamwrite_backend.delete_database(database_name=database_name)
         return "{}"
 
-    def describe_database(self):
+    def describe_database(self) -> str:
         database_name = self._get_param("DatabaseName")
         database = self.timestreamwrite_backend.describe_database(
             database_name=database_name
         )
         return json.dumps(dict(Database=database.description()))
 
-    def update_database(self):
+    def update_database(self) -> str:
         database_name = self._get_param("DatabaseName")
         kms_key_id = self._get_param("KmsKeyId")
         database = self.timestreamwrite_backend.update_database(
@@ -42,11 +42,11 @@ class TimestreamWriteResponse(BaseResponse):
         )
         return json.dumps(dict(Database=database.description()))
 
-    def list_databases(self):
+    def list_databases(self) -> str:
         all_dbs = self.timestreamwrite_backend.list_databases()
         return json.dumps(dict(Databases=[db.description() for db in all_dbs]))
 
-    def create_table(self):
+    def create_table(self) -> str:
         database_name = self._get_param("DatabaseName")
         table_name = self._get_param("TableName")
         retention_properties = self._get_param("RetentionProperties")
@@ -63,24 +63,24 @@ class TimestreamWriteResponse(BaseResponse):
         )
         return json.dumps(dict(Table=table.description()))
 
-    def delete_table(self):
+    def delete_table(self) -> str:
         database_name = self._get_param("DatabaseName")
         table_name = self._get_param("TableName")
         self.timestreamwrite_backend.delete_table(database_name, table_name)
         return "{}"
 
-    def describe_table(self):
+    def describe_table(self) -> str:
         database_name = self._get_param("DatabaseName")
         table_name = self._get_param("TableName")
         table = self.timestreamwrite_backend.describe_table(database_name, table_name)
         return json.dumps(dict(Table=table.description()))
 
-    def list_tables(self):
+    def list_tables(self) -> str:
         database_name = self._get_param("DatabaseName")
         tables = self.timestreamwrite_backend.list_tables(database_name)
         return json.dumps(dict(Tables=[t.description() for t in tables]))
 
-    def update_table(self):
+    def update_table(self) -> str:
         database_name = self._get_param("DatabaseName")
         table_name = self._get_param("TableName")
         retention_properties = self._get_param("RetentionProperties")
@@ -95,7 +95,7 @@ class TimestreamWriteResponse(BaseResponse):
         )
         return json.dumps(dict(Table=table.description()))
 
-    def write_records(self):
+    def write_records(self) -> str:
         database_name = self._get_param("DatabaseName")
         table_name = self._get_param("TableName")
         records = self._get_param("Records")
@@ -109,22 +109,22 @@ class TimestreamWriteResponse(BaseResponse):
         }
         return json.dumps(resp)
 
-    def describe_endpoints(self):
+    def describe_endpoints(self) -> str:
         resp = self.timestreamwrite_backend.describe_endpoints()
         return json.dumps(resp)
 
-    def list_tags_for_resource(self):
+    def list_tags_for_resource(self) -> str:
         resource_arn = self._get_param("ResourceARN")
         tags = self.timestreamwrite_backend.list_tags_for_resource(resource_arn)
         return json.dumps(tags)
 
-    def tag_resource(self):
+    def tag_resource(self) -> str:
         resource_arn = self._get_param("ResourceARN")
         tags = self._get_param("Tags")
         self.timestreamwrite_backend.tag_resource(resource_arn, tags)
         return "{}"
 
-    def untag_resource(self):
+    def untag_resource(self) -> str:
         resource_arn = self._get_param("ResourceARN")
         tag_keys = self._get_param("TagKeys")
         self.timestreamwrite_backend.untag_resource(resource_arn, tag_keys)
diff --git a/moto/transcribe/exceptions.py b/moto/transcribe/exceptions.py
index 948f5665b..d8381e8db 100644
--- a/moto/transcribe/exceptions.py
+++ b/moto/transcribe/exceptions.py
@@ -2,10 +2,10 @@ from moto.core.exceptions import JsonRESTError
 
 
 class ConflictException(JsonRESTError):
-    def __init__(self, message, **kwargs):
-        super().__init__("ConflictException", message, **kwargs)
+    def __init__(self, message: str):
+        super().__init__("ConflictException", message)
 
 
 class BadRequestException(JsonRESTError):
-    def __init__(self, message, **kwargs):
-        super().__init__("BadRequestException", message, **kwargs)
+    def __init__(self, message: str):
+        super().__init__("BadRequestException", message)
diff --git a/moto/transcribe/models.py b/moto/transcribe/models.py
index 498389eff..75c1bb955 100644
--- a/moto/transcribe/models.py
+++ b/moto/transcribe/models.py
@@ -1,4 +1,5 @@
 from datetime import datetime, timedelta
+from typing import Any, Dict, List, Optional
 from moto.core import BaseBackend, BackendDict, BaseModel
 from moto.moto_api import state_manager
 from moto.moto_api._internal import mock_random
@@ -7,14 +8,14 @@ from .exceptions import ConflictException, BadRequestException
 
 
 class BaseObject(BaseModel):
-    def camelCase(self, key):
+    def camelCase(self, key: str) -> str:
         words = []
         for word in key.split("_"):
             words.append(word.title())
         return "".join(words)
 
-    def gen_response_object(self):
-        response_object = dict()
+    def gen_response_object(self) -> Dict[str, Any]:
+        response_object: Dict[str, Any] = dict()
         for key, value in self.__dict__.items():
             if "_" in key:
                 response_object[self.camelCase(key)] = value
@@ -23,30 +24,30 @@ class BaseObject(BaseModel):
         return response_object
 
     @property
-    def response_object(self):
+    def response_object(self) -> Dict[str, Any]:  # type: ignore[misc]
         return self.gen_response_object()
 
 
 class FakeTranscriptionJob(BaseObject, ManagedState):
     def __init__(
         self,
-        account_id,
-        region_name,
-        transcription_job_name,
-        language_code,
-        media_sample_rate_hertz,
-        media_format,
-        media,
-        output_bucket_name,
-        output_key,
-        output_encryption_kms_key_id,
-        settings,
-        model_settings,
-        job_execution_settings,
-        content_redaction,
-        identify_language,
-        identify_multiple_languages,
-        language_options,
+        account_id: str,
+        region_name: str,
+        transcription_job_name: str,
+        language_code: Optional[str],
+        media_sample_rate_hertz: Optional[int],
+        media_format: Optional[str],
+        media: Dict[str, str],
+        output_bucket_name: Optional[str],
+        output_key: Optional[str],
+        output_encryption_kms_key_id: Optional[str],
+        settings: Optional[Dict[str, Any]],
+        model_settings: Optional[Dict[str, Optional[str]]],
+        job_execution_settings: Optional[Dict[str, Any]],
+        content_redaction: Optional[Dict[str, Any]],
+        identify_language: Optional[bool],
+        identify_multiple_languages: Optional[bool],
+        language_options: Optional[List[str]],
     ):
         ManagedState.__init__(
             self,
@@ -61,12 +62,13 @@ class FakeTranscriptionJob(BaseObject, ManagedState):
         self._region_name = region_name
         self.transcription_job_name = transcription_job_name
         self.language_code = language_code
-        self.language_codes = None
+        self.language_codes: Optional[List[Dict[str, Any]]] = None
         self.media_sample_rate_hertz = media_sample_rate_hertz
         self.media_format = media_format
         self.media = media
-        self.transcript = None
-        self.start_time = self.completion_time = None
+        self.transcript: Optional[Dict[str, str]] = None
+        self.start_time: Optional[str] = None
+        self.completion_time: Optional[str] = None
         self.creation_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
         self.failure_reason = None
         self.settings = settings or {
@@ -86,7 +88,7 @@ class FakeTranscriptionJob(BaseObject, ManagedState):
         self.identify_language = identify_language
         self.identify_multiple_languages = identify_multiple_languages
         self.language_options = language_options
-        self.identified_language_score = (None,)
+        self.identified_language_score: Optional[float] = None
         self._output_bucket_name = output_bucket_name
         self.output_key = output_key
         self._output_encryption_kms_key_id = output_encryption_kms_key_id
@@ -94,7 +96,7 @@ class FakeTranscriptionJob(BaseObject, ManagedState):
             "CUSTOMER_BUCKET" if self._output_bucket_name else "SERVICE_BUCKET"
         )
 
-    def response_object(self, response_type):
+    def response_object(self, response_type: str) -> Dict[str, Any]:  # type: ignore
         response_field_dict = {
             "CREATE": [
                 "TranscriptionJobName",
@@ -162,7 +164,7 @@ class FakeTranscriptionJob(BaseObject, ManagedState):
                 if k in response_fields and v is not None and v != [None]
             }
 
-    def advance(self):
+    def advance(self) -> None:
         old_status = self.status
         super().advance()
         new_status = self.status
@@ -191,20 +193,20 @@ class FakeTranscriptionJob(BaseObject, ManagedState):
                 self.identified_language_score = 0.999645948
                 # Identify first two languages passed in language_options
                 # If none is set, default to "en-US"
-                self.language_codes = []
+                self.language_codes: List[Dict[str, Any]] = []  # type: ignore[no-redef]
                 if self.language_options is None or len(self.language_options) == 0:
-                    self.language_codes.append(
+                    self.language_codes.append(  # type: ignore
                         {"LanguageCode": "en-US", "DurationInSeconds": 123.0}
                     )
                 else:
-                    self.language_codes.append(
+                    self.language_codes.append(  # type: ignore
                         {
                             "LanguageCode": self.language_options[0],
                             "DurationInSeconds": 123.0,
                         }
                     )
                     if len(self.language_options) > 1:
-                        self.language_codes.append(
+                        self.language_codes.append(  # type: ignore
                             {
                                 "LanguageCode": self.language_options[1],
                                 "DurationInSeconds": 321.0,
@@ -229,12 +231,12 @@ class FakeTranscriptionJob(BaseObject, ManagedState):
 class FakeVocabulary(BaseObject, ManagedState):
     def __init__(
         self,
-        account_id,
-        region_name,
-        vocabulary_name,
-        language_code,
-        phrases,
-        vocabulary_file_uri,
+        account_id: str,
+        region_name: str,
+        vocabulary_name: str,
+        language_code: str,
+        phrases: Optional[List[str]],
+        vocabulary_file_uri: Optional[str],
     ):
         # Configured ManagedState
         super().__init__(
@@ -247,11 +249,11 @@ class FakeVocabulary(BaseObject, ManagedState):
         self.language_code = language_code
         self.phrases = phrases
         self.vocabulary_file_uri = vocabulary_file_uri
-        self.last_modified_time = None
+        self.last_modified_time: Optional[str] = None
         self.failure_reason = None
         self.download_uri = f"https://s3.{region_name}.amazonaws.com/aws-transcribe-dictionary-model-{region_name}-prod/{account_id}/{vocabulary_name}/{mock_random.uuid4()}/input.txt"
 
-    def response_object(self, response_type):
+    def response_object(self, response_type: str) -> Dict[str, Any]:  # type: ignore
         response_field_dict = {
             "CREATE": [
                 "VocabularyName",
@@ -284,7 +286,7 @@ class FakeVocabulary(BaseObject, ManagedState):
             if k in response_fields and v is not None and v != [None]
         }
 
-    def advance(self):
+    def advance(self) -> None:
         old_status = self.status
         super().advance()
         new_status = self.status
@@ -296,17 +298,17 @@ class FakeVocabulary(BaseObject, ManagedState):
 class FakeMedicalTranscriptionJob(BaseObject, ManagedState):
     def __init__(
         self,
-        region_name,
-        medical_transcription_job_name,
-        language_code,
-        media_sample_rate_hertz,
-        media_format,
-        media,
-        output_bucket_name,
-        output_encryption_kms_key_id,
-        settings,
-        specialty,
-        job_type,
+        region_name: str,
+        medical_transcription_job_name: str,
+        language_code: str,
+        media_sample_rate_hertz: Optional[int],
+        media_format: Optional[str],
+        media: Dict[str, str],
+        output_bucket_name: str,
+        output_encryption_kms_key_id: Optional[str],
+        settings: Optional[Dict[str, Any]],
+        specialty: str,
+        job_type: str,
     ):
         ManagedState.__init__(
             self,
@@ -323,8 +325,9 @@ class FakeMedicalTranscriptionJob(BaseObject, ManagedState):
         self.media_sample_rate_hertz = media_sample_rate_hertz
         self.media_format = media_format
         self.media = media
-        self.transcript = None
-        self.start_time = self.completion_time = None
+        self.transcript: Optional[Dict[str, str]] = None
+        self.start_time: Optional[str] = None
+        self.completion_time: Optional[str] = None
         self.creation_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
         self.failure_reason = None
         self.settings = settings or {
@@ -337,7 +340,7 @@ class FakeMedicalTranscriptionJob(BaseObject, ManagedState):
         self._output_encryption_kms_key_id = output_encryption_kms_key_id
         self.output_location_type = "CUSTOMER_BUCKET"
 
-    def response_object(self, response_type):
+    def response_object(self, response_type: str) -> Dict[str, Any]:  # type: ignore
         response_field_dict = {
             "CREATE": [
                 "MedicalTranscriptionJobName",
@@ -396,7 +399,7 @@ class FakeMedicalTranscriptionJob(BaseObject, ManagedState):
                 if k in response_fields and v is not None and v != [None]
             }
 
-    def advance(self):
+    def advance(self) -> None:
         old_status = self.status
         super().advance()
         new_status = self.status
@@ -425,11 +428,11 @@ class FakeMedicalTranscriptionJob(BaseObject, ManagedState):
 class FakeMedicalVocabulary(FakeVocabulary):
     def __init__(
         self,
-        account_id,
-        region_name,
-        vocabulary_name,
-        language_code,
-        vocabulary_file_uri,
+        account_id: str,
+        region_name: str,
+        vocabulary_name: str,
+        language_code: str,
+        vocabulary_file_uri: Optional[str],
     ):
         super().__init__(
             account_id,
@@ -450,12 +453,12 @@ class FakeMedicalVocabulary(FakeVocabulary):
 
 
 class TranscribeBackend(BaseBackend):
-    def __init__(self, region_name, account_id):
+    def __init__(self, region_name: str, account_id: str):
         super().__init__(region_name, account_id)
-        self.medical_transcriptions = {}
-        self.transcriptions = {}
-        self.medical_vocabularies = {}
-        self.vocabularies = {}
+        self.medical_transcriptions: Dict[str, FakeMedicalTranscriptionJob] = {}
+        self.transcriptions: Dict[str, FakeTranscriptionJob] = {}
+        self.medical_vocabularies: Dict[str, FakeMedicalVocabulary] = {}
+        self.vocabularies: Dict[str, FakeVocabulary] = {}
 
         state_manager.register_default_transition(
             "transcribe::vocabulary", transition={"progression": "manual", "times": 1}
@@ -474,7 +477,9 @@ class TranscribeBackend(BaseBackend):
         )
 
     @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 services."""
         return BaseBackend.default_vpc_endpoint_service_factory(
             service_region, zones, "transcribe"
@@ -482,15 +487,29 @@ class TranscribeBackend(BaseBackend):
             service_region, zones, "transcribestreaming"
         )
 
-    def start_transcription_job(self, **kwargs):
-
-        name = kwargs.get("transcription_job_name")
-        if name in self.transcriptions:
+    def start_transcription_job(
+        self,
+        transcription_job_name: str,
+        language_code: Optional[str],
+        media_sample_rate_hertz: Optional[int],
+        media_format: Optional[str],
+        media: Dict[str, str],
+        output_bucket_name: Optional[str],
+        output_key: Optional[str],
+        output_encryption_kms_key_id: Optional[str],
+        settings: Optional[Dict[str, Any]],
+        model_settings: Optional[Dict[str, Optional[str]]],
+        job_execution_settings: Optional[Dict[str, Any]],
+        content_redaction: Optional[Dict[str, Any]],
+        identify_language: Optional[bool],
+        identify_multiple_languages: Optional[bool],
+        language_options: Optional[List[str]],
+    ) -> Dict[str, Any]:
+        if transcription_job_name in self.transcriptions:
             raise ConflictException(
                 message="The requested job name already exists. Use a different job name."
             )
 
-        settings = kwargs.get("settings")
         vocabulary_name = settings.get("VocabularyName") if settings else None
         if vocabulary_name and vocabulary_name not in self.vocabularies:
             raise BadRequestException(
@@ -501,36 +520,45 @@ class TranscribeBackend(BaseBackend):
         transcription_job_object = FakeTranscriptionJob(
             account_id=self.account_id,
             region_name=self.region_name,
-            transcription_job_name=name,
-            language_code=kwargs.get("language_code"),
-            media_sample_rate_hertz=kwargs.get("media_sample_rate_hertz"),
-            media_format=kwargs.get("media_format"),
-            media=kwargs.get("media"),
-            output_bucket_name=kwargs.get("output_bucket_name"),
-            output_key=kwargs.get("output_key"),
-            output_encryption_kms_key_id=kwargs.get("output_encryption_kms_key_id"),
+            transcription_job_name=transcription_job_name,
+            language_code=language_code,
+            media_sample_rate_hertz=media_sample_rate_hertz,
+            media_format=media_format,
+            media=media,
+            output_bucket_name=output_bucket_name,
+            output_key=output_key,
+            output_encryption_kms_key_id=output_encryption_kms_key_id,
             settings=settings,
-            model_settings=kwargs.get("model_settings"),
-            job_execution_settings=kwargs.get("job_execution_settings"),
-            content_redaction=kwargs.get("content_redaction"),
-            identify_language=kwargs.get("identify_language"),
-            identify_multiple_languages=kwargs.get("identify_multiple_languages"),
-            language_options=kwargs.get("language_options"),
+            model_settings=model_settings,
+            job_execution_settings=job_execution_settings,
+            content_redaction=content_redaction,
+            identify_language=identify_language,
+            identify_multiple_languages=identify_multiple_languages,
+            language_options=language_options,
         )
-        self.transcriptions[name] = transcription_job_object
+        self.transcriptions[transcription_job_name] = transcription_job_object
 
         return transcription_job_object.response_object("CREATE")
 
-    def start_medical_transcription_job(self, **kwargs):
-
-        name = kwargs.get("medical_transcription_job_name")
-
-        if name in self.medical_transcriptions:
+    def start_medical_transcription_job(
+        self,
+        medical_transcription_job_name: str,
+        language_code: str,
+        media_sample_rate_hertz: Optional[int],
+        media_format: Optional[str],
+        media: Dict[str, str],
+        output_bucket_name: str,
+        output_encryption_kms_key_id: Optional[str],
+        settings: Optional[Dict[str, Any]],
+        specialty: str,
+        type_: str,
+    ) -> Dict[str, Any]:
+
+        if medical_transcription_job_name in self.medical_transcriptions:
             raise ConflictException(
                 message="The requested job name already exists. Use a different job name."
             )
 
-        settings = kwargs.get("settings")
         vocabulary_name = settings.get("VocabularyName") if settings else None
         if vocabulary_name and vocabulary_name not in self.medical_vocabularies:
             raise BadRequestException(
@@ -540,23 +568,25 @@ class TranscribeBackend(BaseBackend):
 
         transcription_job_object = FakeMedicalTranscriptionJob(
             region_name=self.region_name,
-            medical_transcription_job_name=name,
-            language_code=kwargs.get("language_code"),
-            media_sample_rate_hertz=kwargs.get("media_sample_rate_hertz"),
-            media_format=kwargs.get("media_format"),
-            media=kwargs.get("media"),
-            output_bucket_name=kwargs.get("output_bucket_name"),
-            output_encryption_kms_key_id=kwargs.get("output_encryption_kms_key_id"),
+            medical_transcription_job_name=medical_transcription_job_name,
+            language_code=language_code,
+            media_sample_rate_hertz=media_sample_rate_hertz,
+            media_format=media_format,
+            media=media,
+            output_bucket_name=output_bucket_name,
+            output_encryption_kms_key_id=output_encryption_kms_key_id,
             settings=settings,
-            specialty=kwargs.get("specialty"),
-            job_type=kwargs.get("type"),
+            specialty=specialty,
+            job_type=type_,
         )
 
-        self.medical_transcriptions[name] = transcription_job_object
+        self.medical_transcriptions[
+            medical_transcription_job_name
+        ] = transcription_job_object
 
         return transcription_job_object.response_object("CREATE")
 
-    def get_transcription_job(self, transcription_job_name):
+    def get_transcription_job(self, transcription_job_name: str) -> Dict[str, Any]:
         try:
             job = self.transcriptions[transcription_job_name]
             job.advance()  # Fakes advancement through statuses.
@@ -567,7 +597,9 @@ class TranscribeBackend(BaseBackend):
                 "Check the job name and try your request again."
             )
 
-    def get_medical_transcription_job(self, medical_transcription_job_name):
+    def get_medical_transcription_job(
+        self, medical_transcription_job_name: str
+    ) -> Dict[str, Any]:
         try:
             job = self.medical_transcriptions[medical_transcription_job_name]
             job.advance()  # Fakes advancement through statuses.
@@ -578,7 +610,7 @@ class TranscribeBackend(BaseBackend):
                 "Check the job name and try your request again."
             )
 
-    def delete_transcription_job(self, transcription_job_name):
+    def delete_transcription_job(self, transcription_job_name: str) -> None:
         try:
             del self.transcriptions[transcription_job_name]
         except KeyError:
@@ -587,7 +619,9 @@ class TranscribeBackend(BaseBackend):
                 "Check the job name and try your request again."
             )
 
-    def delete_medical_transcription_job(self, medical_transcription_job_name):
+    def delete_medical_transcription_job(
+        self, medical_transcription_job_name: str
+    ) -> None:
         try:
             del self.medical_transcriptions[medical_transcription_job_name]
         except KeyError:
@@ -597,8 +631,12 @@ class TranscribeBackend(BaseBackend):
             )
 
     def list_transcription_jobs(
-        self, state_equals, job_name_contains, next_token, max_results
-    ):
+        self,
+        state_equals: str,
+        job_name_contains: str,
+        next_token: str,
+        max_results: int,
+    ) -> Dict[str, Any]:
         jobs = list(self.transcriptions.values())
 
         if state_equals:
@@ -615,7 +653,7 @@ class TranscribeBackend(BaseBackend):
         )  # Arbitrarily selected...
         jobs_paginated = jobs[start_offset:end_offset]
 
-        response = {
+        response: Dict[str, Any] = {
             "TranscriptionJobSummaries": [
                 job.response_object("LIST") for job in jobs_paginated
             ]
@@ -627,8 +665,8 @@ class TranscribeBackend(BaseBackend):
         return response
 
     def list_medical_transcription_jobs(
-        self, status, job_name_contains, next_token, max_results
-    ):
+        self, status: str, job_name_contains: str, next_token: str, max_results: int
+    ) -> Dict[str, Any]:
         jobs = list(self.medical_transcriptions.values())
 
         if status:
@@ -647,7 +685,7 @@ class TranscribeBackend(BaseBackend):
         )  # Arbitrarily selected...
         jobs_paginated = jobs[start_offset:end_offset]
 
-        response = {
+        response: Dict[str, Any] = {
             "MedicalTranscriptionJobSummaries": [
                 job.response_object("LIST") for job in jobs_paginated
             ]
@@ -658,12 +696,13 @@ class TranscribeBackend(BaseBackend):
             response["Status"] = status
         return response
 
-    def create_vocabulary(self, **kwargs):
-
-        vocabulary_name = kwargs.get("vocabulary_name")
-        language_code = kwargs.get("language_code")
-        phrases = kwargs.get("phrases")
-        vocabulary_file_uri = kwargs.get("vocabulary_file_uri")
+    def create_vocabulary(
+        self,
+        vocabulary_name: str,
+        language_code: str,
+        phrases: Optional[List[str]],
+        vocabulary_file_uri: Optional[str],
+    ) -> Dict[str, Any]:
         if (
             phrases is not None
             and vocabulary_file_uri is not None
@@ -698,12 +737,12 @@ class TranscribeBackend(BaseBackend):
 
         return vocabulary_object.response_object("CREATE")
 
-    def create_medical_vocabulary(self, **kwargs):
-
-        vocabulary_name = kwargs.get("vocabulary_name")
-        language_code = kwargs.get("language_code")
-        vocabulary_file_uri = kwargs.get("vocabulary_file_uri")
-
+    def create_medical_vocabulary(
+        self,
+        vocabulary_name: str,
+        language_code: str,
+        vocabulary_file_uri: Optional[str],
+    ) -> Dict[str, Any]:
         if vocabulary_name in self.medical_vocabularies:
             raise ConflictException(
                 message="The requested vocabulary name already exists. "
@@ -722,7 +761,7 @@ class TranscribeBackend(BaseBackend):
 
         return medical_vocabulary_object.response_object("CREATE")
 
-    def get_vocabulary(self, vocabulary_name):
+    def get_vocabulary(self, vocabulary_name: str) -> Dict[str, Any]:
         try:
             job = self.vocabularies[vocabulary_name]
             job.advance()  # Fakes advancement through statuses.
@@ -733,7 +772,7 @@ class TranscribeBackend(BaseBackend):
                 "Check the vocabulary name and try your request again."
             )
 
-    def get_medical_vocabulary(self, vocabulary_name):
+    def get_medical_vocabulary(self, vocabulary_name: str) -> Dict[str, Any]:
         try:
             job = self.medical_vocabularies[vocabulary_name]
             job.advance()  # Fakes advancement through statuses.
@@ -744,7 +783,7 @@ class TranscribeBackend(BaseBackend):
                 "Check the vocabulary name and try your request again."
             )
 
-    def delete_vocabulary(self, vocabulary_name):
+    def delete_vocabulary(self, vocabulary_name: str) -> None:
         try:
             del self.vocabularies[vocabulary_name]
         except KeyError:
@@ -752,7 +791,7 @@ class TranscribeBackend(BaseBackend):
                 message="The requested vocabulary couldn't be found. Check the vocabulary name and try your request again."
             )
 
-    def delete_medical_vocabulary(self, vocabulary_name):
+    def delete_medical_vocabulary(self, vocabulary_name: str) -> None:
         try:
             del self.medical_vocabularies[vocabulary_name]
         except KeyError:
@@ -760,7 +799,9 @@ class TranscribeBackend(BaseBackend):
                 message="The requested vocabulary couldn't be found. Check the vocabulary name and try your request again."
             )
 
-    def list_vocabularies(self, state_equals, name_contains, next_token, max_results):
+    def list_vocabularies(
+        self, state_equals: str, name_contains: str, next_token: str, max_results: int
+    ) -> Dict[str, Any]:
         vocabularies = list(self.vocabularies.values())
 
         if state_equals:
@@ -783,7 +824,7 @@ class TranscribeBackend(BaseBackend):
         )  # Arbitrarily selected...
         vocabularies_paginated = vocabularies[start_offset:end_offset]
 
-        response = {
+        response: Dict[str, Any] = {
             "Vocabularies": [
                 vocabulary.response_object("LIST")
                 for vocabulary in vocabularies_paginated
@@ -796,8 +837,8 @@ class TranscribeBackend(BaseBackend):
         return response
 
     def list_medical_vocabularies(
-        self, state_equals, name_contains, next_token, max_results
-    ):
+        self, state_equals: str, name_contains: str, next_token: str, max_results: int
+    ) -> Dict[str, Any]:
         vocabularies = list(self.medical_vocabularies.values())
 
         if state_equals:
@@ -820,7 +861,7 @@ class TranscribeBackend(BaseBackend):
         )  # Arbitrarily selected...
         vocabularies_paginated = vocabularies[start_offset:end_offset]
 
-        response = {
+        response: Dict[str, Any] = {
             "Vocabularies": [
                 vocabulary.response_object("LIST")
                 for vocabulary in vocabularies_paginated
diff --git a/moto/transcribe/responses.py b/moto/transcribe/responses.py
index 6fb54c397..a74771c3a 100644
--- a/moto/transcribe/responses.py
+++ b/moto/transcribe/responses.py
@@ -2,26 +2,19 @@ import json
 
 from moto.core.responses import BaseResponse
 from moto.utilities.aws_headers import amzn_request_id
-from .models import transcribe_backends
+from .models import transcribe_backends, TranscribeBackend
 
 
 class TranscribeResponse(BaseResponse):
-    def __init__(self):
+    def __init__(self) -> None:
         super().__init__(service_name="transcribe")
 
     @property
-    def transcribe_backend(self):
+    def transcribe_backend(self) -> TranscribeBackend:
         return transcribe_backends[self.current_account][self.region]
 
-    @property
-    def request_params(self):
-        try:
-            return json.loads(self.body)
-        except ValueError:
-            return {}
-
     @amzn_request_id
-    def start_transcription_job(self):
+    def start_transcription_job(self) -> str:
         name = self._get_param("TranscriptionJobName")
         response = self.transcribe_backend.start_transcription_job(
             transcription_job_name=name,
@@ -43,7 +36,7 @@ class TranscribeResponse(BaseResponse):
         return json.dumps(response)
 
     @amzn_request_id
-    def start_medical_transcription_job(self):
+    def start_medical_transcription_job(self) -> str:
         name = self._get_param("MedicalTranscriptionJobName")
         response = self.transcribe_backend.start_medical_transcription_job(
             medical_transcription_job_name=name,
@@ -55,12 +48,12 @@ class TranscribeResponse(BaseResponse):
             output_encryption_kms_key_id=self._get_param("OutputEncryptionKMSKeyId"),
             settings=self._get_param("Settings"),
             specialty=self._get_param("Specialty"),
-            type=self._get_param("Type"),
+            type_=self._get_param("Type"),
         )
         return json.dumps(response)
 
     @amzn_request_id
-    def list_transcription_jobs(self):
+    def list_transcription_jobs(self) -> str:
         state_equals = self._get_param("Status")
         job_name_contains = self._get_param("JobNameContains")
         next_token = self._get_param("NextToken")
@@ -75,7 +68,7 @@ class TranscribeResponse(BaseResponse):
         return json.dumps(response)
 
     @amzn_request_id
-    def list_medical_transcription_jobs(self):
+    def list_medical_transcription_jobs(self) -> str:
         status = self._get_param("Status")
         job_name_contains = self._get_param("JobNameContains")
         next_token = self._get_param("NextToken")
@@ -90,7 +83,7 @@ class TranscribeResponse(BaseResponse):
         return json.dumps(response)
 
     @amzn_request_id
-    def get_transcription_job(self):
+    def get_transcription_job(self) -> str:
         transcription_job_name = self._get_param("TranscriptionJobName")
         response = self.transcribe_backend.get_transcription_job(
             transcription_job_name=transcription_job_name
@@ -98,7 +91,7 @@ class TranscribeResponse(BaseResponse):
         return json.dumps(response)
 
     @amzn_request_id
-    def get_medical_transcription_job(self):
+    def get_medical_transcription_job(self) -> str:
         medical_transcription_job_name = self._get_param("MedicalTranscriptionJobName")
         response = self.transcribe_backend.get_medical_transcription_job(
             medical_transcription_job_name=medical_transcription_job_name
@@ -106,23 +99,23 @@ class TranscribeResponse(BaseResponse):
         return json.dumps(response)
 
     @amzn_request_id
-    def delete_transcription_job(self):
+    def delete_transcription_job(self) -> str:
         transcription_job_name = self._get_param("TranscriptionJobName")
-        response = self.transcribe_backend.delete_transcription_job(
+        self.transcribe_backend.delete_transcription_job(
             transcription_job_name=transcription_job_name
         )
-        return json.dumps(response)
+        return "{}"
 
     @amzn_request_id
-    def delete_medical_transcription_job(self):
+    def delete_medical_transcription_job(self) -> str:
         medical_transcription_job_name = self._get_param("MedicalTranscriptionJobName")
-        response = self.transcribe_backend.delete_medical_transcription_job(
+        self.transcribe_backend.delete_medical_transcription_job(
             medical_transcription_job_name=medical_transcription_job_name
         )
-        return json.dumps(response)
+        return "{}"
 
     @amzn_request_id
-    def create_vocabulary(self):
+    def create_vocabulary(self) -> str:
         vocabulary_name = self._get_param("VocabularyName")
         language_code = self._get_param("LanguageCode")
         phrases = self._get_param("Phrases")
@@ -136,7 +129,7 @@ class TranscribeResponse(BaseResponse):
         return json.dumps(response)
 
     @amzn_request_id
-    def create_medical_vocabulary(self):
+    def create_medical_vocabulary(self) -> str:
         vocabulary_name = self._get_param("VocabularyName")
         language_code = self._get_param("LanguageCode")
         vocabulary_file_uri = self._get_param("VocabularyFileUri")
@@ -148,7 +141,7 @@ class TranscribeResponse(BaseResponse):
         return json.dumps(response)
 
     @amzn_request_id
-    def get_vocabulary(self):
+    def get_vocabulary(self) -> str:
         vocabulary_name = self._get_param("VocabularyName")
         response = self.transcribe_backend.get_vocabulary(
             vocabulary_name=vocabulary_name
@@ -156,7 +149,7 @@ class TranscribeResponse(BaseResponse):
         return json.dumps(response)
 
     @amzn_request_id
-    def get_medical_vocabulary(self):
+    def get_medical_vocabulary(self) -> str:
         vocabulary_name = self._get_param("VocabularyName")
         response = self.transcribe_backend.get_medical_vocabulary(
             vocabulary_name=vocabulary_name
@@ -164,7 +157,7 @@ class TranscribeResponse(BaseResponse):
         return json.dumps(response)
 
     @amzn_request_id
-    def list_vocabularies(self):
+    def list_vocabularies(self) -> str:
         state_equals = self._get_param("StateEquals")
         name_contains = self._get_param("NameContains")
         next_token = self._get_param("NextToken")
@@ -179,7 +172,7 @@ class TranscribeResponse(BaseResponse):
         return json.dumps(response)
 
     @amzn_request_id
-    def list_medical_vocabularies(self):
+    def list_medical_vocabularies(self) -> str:
         state_equals = self._get_param("StateEquals")
         name_contains = self._get_param("NameContains")
         next_token = self._get_param("NextToken")
@@ -194,17 +187,15 @@ class TranscribeResponse(BaseResponse):
         return json.dumps(response)
 
     @amzn_request_id
-    def delete_vocabulary(self):
+    def delete_vocabulary(self) -> str:
         vocabulary_name = self._get_param("VocabularyName")
-        response = self.transcribe_backend.delete_vocabulary(
-            vocabulary_name=vocabulary_name
-        )
-        return json.dumps(response)
+        self.transcribe_backend.delete_vocabulary(vocabulary_name=vocabulary_name)
+        return "{}"
 
     @amzn_request_id
-    def delete_medical_vocabulary(self):
+    def delete_medical_vocabulary(self) -> str:
         vocabulary_name = self._get_param("VocabularyName")
-        response = self.transcribe_backend.delete_medical_vocabulary(
+        self.transcribe_backend.delete_medical_vocabulary(
             vocabulary_name=vocabulary_name
         )
-        return json.dumps(response)
+        return "{}"
diff --git a/moto/utilities/aws_headers.py b/moto/utilities/aws_headers.py
index f3366e499..5f7350e2b 100644
--- a/moto/utilities/aws_headers.py
+++ b/moto/utilities/aws_headers.py
@@ -1,5 +1,10 @@
 from functools import wraps
-from typing import Any, Callable, TypeVar
+from typing import Any, Callable, Dict, TypeVar, Optional, TYPE_CHECKING
+
+if TYPE_CHECKING:
+    from typing_extensions import Protocol
+else:
+    Protocol = object
 
 import binascii
 import re
@@ -9,7 +14,12 @@ from moto.moto_api._internal import mock_random as random
 TypeDec = TypeVar("TypeDec", bound=Callable[..., Any])
 
 
-def gen_amz_crc32(response, headerdict=None):
+class GenericFunction(Protocol):
+    def __call__(self, *args: Any, **kwargs: Any) -> Any:
+        ...
+
+
+def gen_amz_crc32(response: Any, headerdict: Optional[Dict[str, Any]] = None) -> int:
     if not isinstance(response, bytes):
         response = response.encode("utf-8")
 
@@ -21,7 +31,7 @@ def gen_amz_crc32(response, headerdict=None):
     return crc
 
 
-def gen_amzn_requestid_long(headerdict=None):
+def gen_amzn_requestid_long(headerdict: Optional[Dict[str, Any]] = None) -> str:
     req_id = random.get_random_string(length=52)
 
     if headerdict is not None and isinstance(headerdict, dict):
@@ -30,9 +40,9 @@ def gen_amzn_requestid_long(headerdict=None):
     return req_id
 
 
-def amz_crc32(f: TypeDec) -> TypeDec:
+def amz_crc32(f: TypeDec) -> GenericFunction:
     @wraps(f)
-    def _wrapper(*args: Any, **kwargs: Any) -> Any:
+    def _wrapper(*args: Any, **kwargs: Any) -> Any:  # type: ignore[misc]
         response = f(*args, **kwargs)
 
         headers = {}
@@ -58,9 +68,9 @@ def amz_crc32(f: TypeDec) -> TypeDec:
     return _wrapper
 
 
-def amzn_request_id(f: TypeDec) -> TypeDec:
+def amzn_request_id(f: TypeDec) -> GenericFunction:
     @wraps(f)
-    def _wrapper(*args: Any, **kwargs: Any) -> Any:
+    def _wrapper(*args: Any, **kwargs: Any) -> Any:  # type: ignore[misc]
         response = f(*args, **kwargs)
 
         headers = {}
diff --git a/moto/utilities/distutils_version.py b/moto/utilities/distutils_version.py
index 12d6702bd..a1c90766c 100644
--- a/moto/utilities/distutils_version.py
+++ b/moto/utilities/distutils_version.py
@@ -30,7 +30,7 @@ Every version number class implements the following interface:
 """
 
 import re
-from typing import Optional
+from typing import Any, Optional
 
 
 class Version:
@@ -40,39 +40,39 @@ class Version:
     rich comparisons to _cmp.
     """
 
-    def __init__(self, vstring=None):
+    def __init__(self, vstring: Optional[str] = None):
         if vstring:
-            self.parse(vstring)
+            self.parse(vstring)  # type: ignore[attr-defined]
 
-    def __repr__(self):
+    def __repr__(self) -> str:
         return f"{self.__class__.__name__} ('{self}')"
 
-    def __eq__(self, other):
-        c = self._cmp(other)
+    def __eq__(self, other: Any) -> bool:
+        c = self._cmp(other)  # type: ignore[attr-defined]
         if c is NotImplemented:
             return c
         return c == 0
 
-    def __lt__(self, other):
-        c = self._cmp(other)
+    def __lt__(self, other: Any) -> bool:
+        c = self._cmp(other)  # type: ignore[attr-defined]
         if c is NotImplemented:
             return c
         return c < 0
 
-    def __le__(self, other):
-        c = self._cmp(other)
+    def __le__(self, other: Any) -> bool:
+        c = self._cmp(other)  # type: ignore[attr-defined]
         if c is NotImplemented:
             return c
         return c <= 0
 
-    def __gt__(self, other):
-        c = self._cmp(other)
+    def __gt__(self, other: Any) -> bool:
+        c = self._cmp(other)  # type: ignore[attr-defined]
         if c is NotImplemented:
             return c
         return c > 0
 
-    def __ge__(self, other):
-        c = self._cmp(other)
+    def __ge__(self, other: Any) -> bool:
+        c = self._cmp(other)  # type: ignore[attr-defined]
         if c is NotImplemented:
             return c
         return c >= 0
@@ -199,7 +199,7 @@ class LooseVersion(Version):
         if vstring:
             self.parse(vstring)
 
-    def parse(self, vstring):
+    def parse(self, vstring: str) -> None:
         # I've given up on thinking I can reconstruct the version string
         # from the parsed tuple -- so I just store the string here for
         # use by __str__
@@ -213,13 +213,13 @@ class LooseVersion(Version):
 
         self.version = components
 
-    def __str__(self):
+    def __str__(self) -> str:
         return self.vstring
 
-    def __repr__(self):
+    def __repr__(self) -> str:
         return f"LooseVersion ('{self}')"
 
-    def _cmp(self, other):
+    def _cmp(self, other: Any) -> int:  # type: ignore[return]
         if isinstance(other, str):
             other = LooseVersion(other)
 
diff --git a/moto/utilities/docker_utilities.py b/moto/utilities/docker_utilities.py
index df2987985..9a0f28087 100644
--- a/moto/utilities/docker_utilities.py
+++ b/moto/utilities/docker_utilities.py
@@ -1,9 +1,12 @@
 import functools
 import requests.adapters
-from typing import Tuple
+from typing import Any, Tuple, TYPE_CHECKING
 
 from moto import settings
 
+if TYPE_CHECKING:
+    from docker import DockerClient
+
 
 _orig_adapter_send = requests.adapters.HTTPAdapter.send
 
@@ -13,7 +16,7 @@ class DockerModel:
         self.__docker_client = None
 
     @property
-    def docker_client(self):
+    def docker_client(self) -> "DockerClient":  # type: ignore
         if self.__docker_client is None:
             # We should only initiate the Docker Client at runtime.
             # The docker.from_env() call will fall if Docker is not running
@@ -26,11 +29,11 @@ class DockerModel:
             if requests.adapters.HTTPAdapter.send != _orig_adapter_send:
                 _orig_get_adapter = self.docker_client.api.get_adapter
 
-                def replace_adapter_send(*args, **kwargs):
+                def replace_adapter_send(*args: Any, **kwargs: Any) -> Any:
                     adapter = _orig_get_adapter(*args, **kwargs)
 
                     if isinstance(adapter, requests.adapters.HTTPAdapter):
-                        adapter.send = functools.partial(_orig_adapter_send, adapter)
+                        adapter.send = functools.partial(_orig_adapter_send, adapter)  # type: ignore
                     return adapter
 
                 self.docker_client.api.get_adapter = replace_adapter_send
diff --git a/moto/utilities/paginator.py b/moto/utilities/paginator.py
index 2a07f763e..ce411e49d 100644
--- a/moto/utilities/paginator.py
+++ b/moto/utilities/paginator.py
@@ -2,19 +2,23 @@ import inspect
 
 from copy import deepcopy
 from functools import wraps
-from typing import Dict, Any, Callable
+from typing import Any, Dict, List, Tuple, Optional
 
 from botocore.paginate import TokenDecoder, TokenEncoder
 
 from moto.core.exceptions import InvalidToken
 
 
-def paginate(
-    pagination_model: Dict[str, Any], original_function: Callable = None
-) -> Callable:
-    def pagination_decorator(func):
+# This should be typed using ParamSpec
+# https://stackoverflow.com/a/70591060/13245310
+# This currently does not work for our usecase
+# I believe this could be fixed after https://github.com/python/mypy/pull/14903 is accepted
+
+
+def paginate(pagination_model: Dict[str, Any]) -> Any:
+    def pagination_decorator(func: Any) -> Any:
         @wraps(func)
-        def pagination_wrapper(*args, **kwargs):
+        def pagination_wrapper(*args: Any, **kwargs: Any) -> Any:  # type: ignore
 
             method = func.__name__
             model = pagination_model
@@ -59,27 +63,26 @@ def paginate(
 
         return pagination_wrapper
 
-    if original_function:
-        return pagination_decorator(original_function)
-
     return pagination_decorator
 
 
-class Paginator(object):
+class Paginator:
     def __init__(
         self,
-        max_results=None,
-        max_results_default=None,
-        starting_token=None,
-        unique_attribute=None,
-        param_values_to_check=None,
-        fail_on_invalid_token=True,
+        max_results: Any = None,
+        max_results_default: Any = None,
+        starting_token: Any = None,
+        unique_attribute: Any = None,
+        param_values_to_check: Any = None,
+        fail_on_invalid_token: bool = True,
     ):
         self._max_results = max_results if max_results else max_results_default
         self._starting_token = starting_token
-        self._unique_attributes = unique_attribute
-        if not isinstance(unique_attribute, list):
-            self._unique_attributes = [unique_attribute]
+        self._unique_attributes = (
+            unique_attribute
+            if isinstance(unique_attribute, list)
+            else [unique_attribute]
+        )
         self._param_values_to_check = param_values_to_check
         self._fail_on_invalid_token = fail_on_invalid_token
         self._token_encoder = TokenEncoder()
@@ -87,7 +90,7 @@ class Paginator(object):
         self._param_checksum = self._calculate_parameter_checksum()
         self._parsed_token = self._parse_starting_token()
 
-    def _parse_starting_token(self):
+    def _parse_starting_token(self) -> Optional[Dict[str, Any]]:
         if self._starting_token is None:
             return None
         # The starting token is a dict passed as a base64 encoded string.
@@ -101,7 +104,7 @@ class Paginator(object):
             raise InvalidToken(f"Input inconsistent with page token: {str(next_token)}")
         return next_token
 
-    def _raise_exception_if_required(self, token):
+    def _raise_exception_if_required(self, token: Optional[str]) -> None:
         if self._fail_on_invalid_token:
             if isinstance(self._fail_on_invalid_token, type):
                 # we need to raise a custom exception
@@ -115,8 +118,8 @@ class Paginator(object):
                     raise self._fail_on_invalid_token()
             raise InvalidToken("Invalid token")
 
-    def _calculate_parameter_checksum(self):
-        def freeze(o):
+    def _calculate_parameter_checksum(self) -> int:
+        def freeze(o: Any) -> Any:
             if not o:
                 return None
             if isinstance(o, dict):
@@ -129,7 +132,7 @@ class Paginator(object):
 
         return hash(freeze(self._param_values_to_check))
 
-    def _check_predicate(self, item):
+    def _check_predicate(self, item: Any) -> bool:
         if self._parsed_token is None:
             return False
         unique_attributes = self._parsed_token["uniqueAttributes"]
@@ -140,8 +143,8 @@ class Paginator(object):
                 return False
         return True
 
-    def _build_next_token(self, next_item):
-        token_dict = {}
+    def _build_next_token(self, next_item: Any) -> str:
+        token_dict: Dict[str, Any] = {}
         if self._param_checksum:
             token_dict["parameterChecksum"] = self._param_checksum
         range_keys = []
@@ -153,7 +156,7 @@ class Paginator(object):
         token_dict["uniqueAttributes"] = "|".join(range_keys)
         return self._token_encoder.encode(token_dict)
 
-    def paginate(self, results):
+    def paginate(self, results: List[Any]) -> Tuple[List[Any], Optional[str]]:
         index_start = 0
         if self._starting_token:
             try:
diff --git a/moto/utilities/tagging_service.py b/moto/utilities/tagging_service.py
index 970d1795d..66d934084 100644
--- a/moto/utilities/tagging_service.py
+++ b/moto/utilities/tagging_service.py
@@ -12,7 +12,7 @@ class TaggingService:
         self.tag_name = tag_name
         self.key_name = key_name
         self.value_name = value_name
-        self.tags: Dict[str, str] = {}
+        self.tags: Dict[str, Dict[str, Optional[str]]] = {}
 
     def get_tag_dict_for_resource(self, arn: str) -> Dict[str, str]:
         """Return dict of key/value pairs vs. list of key/values dicts."""
@@ -20,7 +20,7 @@ class TaggingService:
         if self.has_tags(arn):
             for key, val in self.tags[arn].items():
                 result[key] = val
-        return result
+        return result  # type: ignore
 
     def list_tags_for_resource(self, arn: str) -> Dict[str, List[Dict[str, str]]]:
         """Return list of tags inside dict with key of "tag_name".
@@ -32,7 +32,7 @@ class TaggingService:
         if self.has_tags(arn):
             for key, val in self.tags[arn].items():
                 result.append({self.key_name: key, self.value_name: val})
-        return {self.tag_name: result}
+        return {self.tag_name: result}  # type: ignore
 
     def delete_all_tags_for_resource(self, arn: str) -> None:
         """Delete all tags associated with given ARN."""
@@ -88,7 +88,7 @@ class TaggingService:
 
     def extract_tag_names(self, tags: List[Dict[str, str]]) -> List[str]:
         """Return list of key names in list of 'tags' key/value dicts."""
-        results = []
+        results: List[str] = []
         if len(tags) == 0:
             return results
         for tag in tags:
@@ -96,9 +96,9 @@ class TaggingService:
                 results.append(tag[self.key_name])
         return results
 
-    def flatten_tag_list(self, tags: List[Dict[str, str]]) -> Dict[str, str]:
+    def flatten_tag_list(self, tags: List[Dict[str, str]]) -> Dict[str, Optional[str]]:
         """Return dict of key/value pairs with 'tag_name', 'value_name'."""
-        result = {}
+        result: Dict[str, Optional[str]] = {}
         for tag in tags:
             if self.value_name in tag:
                 result[tag[self.key_name]] = tag[self.value_name]
diff --git a/moto/utilities/utils.py b/moto/utilities/utils.py
index ec0188493..73891a2fd 100644
--- a/moto/utilities/utils.py
+++ b/moto/utilities/utils.py
@@ -2,8 +2,7 @@ import json
 import hashlib
 import pkgutil
 
-from collections.abc import MutableMapping
-from typing import Any, Dict, List, TypeVar, Tuple, Optional
+from typing import Any, Dict, Iterator, List, TypeVar, Tuple, Optional, MutableMapping
 
 
 def str2bool(v: Any) -> Optional[bool]:
@@ -20,16 +19,14 @@ def load_resource(package: str, resource: str) -> Any:
     Usage:
     load_resource(__name__, "resources/file.json")
     """
-    resource = pkgutil.get_data(package, resource)
-    return json.loads(resource)
+    return json.loads(pkgutil.get_data(package, resource))  # type: ignore
 
 
 def load_resource_as_str(package: str, resource: str) -> str:
-    resource = pkgutil.get_data(package, resource)
-    return resource.decode("utf-8")
+    return pkgutil.get_data(package, resource).decode("utf-8")  # type: ignore
 
 
-def merge_multiple_dicts(*args: Any) -> Dict[str, any]:
+def merge_multiple_dicts(*args: Any) -> Dict[str, Any]:
     result = {}
     for d in args:
         result.update(d)
@@ -68,36 +65,36 @@ def md5_hash(data: Any = None) -> Any:
     """
     args = (data,) if data else ()
     try:
-        return hashlib.md5(*args, usedforsecurity=False)
+        return hashlib.md5(*args, usedforsecurity=False)  # type: ignore
     except TypeError:
         # The usedforsecurity-parameter is only available as of Python 3.9
         return hashlib.md5(*args)
 
 
-class LowercaseDict(MutableMapping):
+class LowercaseDict(MutableMapping[str, Any]):
     """A dictionary that lowercases all keys"""
 
     def __init__(self, *args: Any, **kwargs: Any):
-        self.store = dict()
+        self.store: Dict[str, Any] = dict()
         self.update(dict(*args, **kwargs))  # use the free update to set keys
 
-    def __getitem__(self, key):
+    def __getitem__(self, key: str) -> Any:
         return self.store[self._keytransform(key)]
 
-    def __setitem__(self, key, value):
+    def __setitem__(self, key: str, value: Any) -> None:
         self.store[self._keytransform(key)] = value
 
-    def __delitem__(self, key):
+    def __delitem__(self, key: str) -> None:
         del self.store[self._keytransform(key)]
 
-    def __iter__(self):
+    def __iter__(self) -> Iterator[Any]:
         return iter(self.store)
 
-    def __len__(self):
+    def __len__(self) -> int:
         return len(self.store)
 
-    def __repr__(self):
+    def __repr__(self) -> str:
         return str(self.store)
 
-    def _keytransform(self, key):
+    def _keytransform(self, key: str) -> str:
         return key.lower()
diff --git a/setup.cfg b/setup.cfg
index 32597d466..6d37533a8 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -239,7 +239,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/e*,moto/f*,moto/g*,moto/i*,moto/k*,moto/l*,moto/m*,moto/n*,moto/o*,moto/p*,moto/q*,moto/r*,moto/s*
+files= moto/a*,moto/b*,moto/c*,moto/d*,moto/e*,moto/f*,moto/g*,moto/i*,moto/k*,moto/l*,moto/m*,moto/n*,moto/o*,moto/p*,moto/q*,moto/r*,moto/s*,moto/u*,moto/t*
 show_column_numbers=True
 show_error_codes = True
 disable_error_code=abstract


Test patch:
diff --git a/tests/test_s3/test_s3_copyobject.py b/tests/test_s3/test_s3_copyobject.py
index 6bb64dee4..e28819b9f 100644
--- a/tests/test_s3/test_s3_copyobject.py
+++ b/tests/test_s3/test_s3_copyobject.py
@@ -1,3 +1,5 @@
+import datetime
+
 import boto3
 from botocore.client import ClientError
 
@@ -408,3 +410,275 @@ def test_copy_object_with_kms_encryption():
     result = client.head_object(Bucket="blah", Key="test2")
     assert result["SSEKMSKeyId"] == kms_key
     assert result["ServerSideEncryption"] == "aws:kms"
+
+
+@mock_s3
+@mock_kms
+def test_copy_object_in_place_with_encryption():
+    kms_client = boto3.client("kms", region_name=DEFAULT_REGION_NAME)
+    s3 = boto3.resource("s3", region_name=DEFAULT_REGION_NAME)
+    client = boto3.client("s3", region_name=DEFAULT_REGION_NAME)
+    kms_key = kms_client.create_key()["KeyMetadata"]["KeyId"]
+    bucket = s3.Bucket("test_bucket")
+    bucket.create()
+    key = "source-key"
+    resp = client.put_object(
+        Bucket="test_bucket",
+        Key=key,
+        Body=b"somedata",
+        ServerSideEncryption="aws:kms",
+        BucketKeyEnabled=True,
+        SSEKMSKeyId=kms_key,
+    )
+    assert resp["BucketKeyEnabled"] is True
+
+    # assert that you can copy in place with the same Encryption settings
+    client.copy_object(
+        Bucket="test_bucket",
+        CopySource=f"test_bucket/{key}",
+        Key=key,
+        ServerSideEncryption="aws:kms",
+        BucketKeyEnabled=True,
+        SSEKMSKeyId=kms_key,
+    )
+
+    # assert that the BucketKeyEnabled setting is not kept in the destination key
+    resp = client.copy_object(
+        Bucket="test_bucket",
+        CopySource=f"test_bucket/{key}",
+        Key=key,
+        ServerSideEncryption="aws:kms",
+        SSEKMSKeyId=kms_key,
+    )
+    assert "BucketKeyEnabled" not in resp
+
+    # this is an edge case, if the source object SSE was not AES256, AWS allows you to not specify any fields
+    # as it will use AES256 by default and is different from the source key
+    resp = client.copy_object(
+        Bucket="test_bucket",
+        CopySource=f"test_bucket/{key}",
+        Key=key,
+    )
+    assert resp["ServerSideEncryption"] == "AES256"
+
+    # check that it allows copying in the place with the same ServerSideEncryption setting as the source
+    resp = client.copy_object(
+        Bucket="test_bucket",
+        CopySource=f"test_bucket/{key}",
+        Key=key,
+        ServerSideEncryption="AES256",
+    )
+    assert resp["ServerSideEncryption"] == "AES256"
+
+
+@mock_s3
+def test_copy_object_in_place_with_storage_class():
+    # this test will validate that setting StorageClass (even the same as source) allows a copy in place
+    s3 = boto3.resource("s3", region_name=DEFAULT_REGION_NAME)
+    client = boto3.client("s3", region_name=DEFAULT_REGION_NAME)
+    bucket_name = "test-bucket"
+    bucket = s3.Bucket(bucket_name)
+    bucket.create()
+    key = "source-key"
+    bucket.put_object(Key=key, Body=b"somedata", StorageClass="STANDARD")
+    client.copy_object(
+        Bucket=bucket_name,
+        CopySource=f"{bucket_name}/{key}",
+        Key=key,
+        StorageClass="STANDARD",
+    )
+    # verify that the copy worked
+    resp = client.get_object_attributes(
+        Bucket=bucket_name, Key=key, ObjectAttributes=["StorageClass"]
+    )
+    assert resp["StorageClass"] == "STANDARD"
+
+
+@mock_s3
+def test_copy_object_does_not_copy_storage_class():
+    s3 = boto3.resource("s3", region_name=DEFAULT_REGION_NAME)
+    client = boto3.client("s3", region_name=DEFAULT_REGION_NAME)
+    bucket = s3.Bucket("test_bucket")
+    bucket.create()
+    source_key = "source-key"
+    dest_key = "dest-key"
+    bucket.put_object(Key=source_key, Body=b"somedata", StorageClass="STANDARD_IA")
+    client.copy_object(
+        Bucket="test_bucket",
+        CopySource=f"test_bucket/{source_key}",
+        Key=dest_key,
+    )
+
+    # Verify that the destination key does not have STANDARD_IA as StorageClass
+    keys = dict([(k.key, k) for k in bucket.objects.all()])
+    keys[source_key].storage_class.should.equal("STANDARD_IA")
+    keys[dest_key].storage_class.should.equal("STANDARD")
+
+
+@mock_s3
+def test_copy_object_does_not_copy_acl():
+    s3 = boto3.resource("s3", region_name=DEFAULT_REGION_NAME)
+    client = boto3.client("s3", region_name=DEFAULT_REGION_NAME)
+    bucket_name = "testbucket"
+    bucket = s3.Bucket(bucket_name)
+    bucket.create()
+    source_key = "source-key"
+    dest_key = "dest-key"
+    control_key = "control-key"
+    # do not set ACL for the control key to get default ACL
+    bucket.put_object(Key=control_key, Body=b"somedata")
+    # set ACL for the source key to check if it will get copied
+    bucket.put_object(Key=source_key, Body=b"somedata", ACL="public-read")
+    # copy object without specifying ACL, so it should get default ACL
+    client.copy_object(
+        Bucket=bucket_name,
+        CopySource=f"{bucket_name}/{source_key}",
+        Key=dest_key,
+    )
+
+    # Get the ACL from the all the keys
+    source_acl = client.get_object_acl(Bucket=bucket_name, Key=source_key)
+    dest_acl = client.get_object_acl(Bucket=bucket_name, Key=dest_key)
+    default_acl = client.get_object_acl(Bucket=bucket_name, Key=control_key)
+    # assert that the source key ACL are different from the destination key ACL
+    assert source_acl["Grants"] != dest_acl["Grants"]
+    # assert that the copied key got the default ACL like the control key
+    assert default_acl["Grants"] == dest_acl["Grants"]
+
+
+@mock_s3
+def test_copy_object_in_place_with_metadata():
+    s3 = boto3.resource("s3", region_name=DEFAULT_REGION_NAME)
+    client = boto3.client("s3", region_name=DEFAULT_REGION_NAME)
+    bucket_name = "testbucket"
+    bucket = s3.Bucket(bucket_name)
+    bucket.create()
+    key_name = "source-key"
+    bucket.put_object(Key=key_name, Body=b"somedata")
+
+    # test that giving metadata is not enough, and should provide MetadataDirective=REPLACE on top
+    with pytest.raises(ClientError) as e:
+        client.copy_object(
+            Bucket=bucket_name,
+            CopySource=f"{bucket_name}/{key_name}",
+            Key=key_name,
+            Metadata={"key": "value"},
+        )
+        e.value.response["Error"]["Message"].should.equal(
+            "This copy request is illegal because it is trying to copy an object to itself without changing the object's metadata, storage class, website redirect location or encryption attributes."
+        )
+
+    # you can only provide MetadataDirective=REPLACE and it will copy without any metadata
+    client.copy_object(
+        Bucket=bucket_name,
+        CopySource=f"{bucket_name}/{key_name}",
+        Key=key_name,
+        MetadataDirective="REPLACE",
+    )
+
+    result = client.head_object(Bucket=bucket_name, Key=key_name)
+    assert result["Metadata"] == {}
+
+
+@mock_s3
+def test_copy_objet_legal_hold():
+    client = boto3.client("s3", region_name=DEFAULT_REGION_NAME)
+    bucket_name = "testbucket"
+    source_key = "source-key"
+    dest_key = "dest-key"
+    client.create_bucket(Bucket=bucket_name, ObjectLockEnabledForBucket=True)
+    client.put_object(
+        Bucket=bucket_name,
+        Key=source_key,
+        Body=b"somedata",
+        ObjectLockLegalHoldStatus="ON",
+    )
+
+    head_object = client.head_object(Bucket=bucket_name, Key=source_key)
+    assert head_object["ObjectLockLegalHoldStatus"] == "ON"
+    assert "VersionId" in head_object
+    version_id = head_object["VersionId"]
+
+    resp = client.copy_object(
+        Bucket=bucket_name,
+        CopySource=f"{bucket_name}/{source_key}",
+        Key=dest_key,
+    )
+    assert resp["CopySourceVersionId"] == version_id
+    assert resp["VersionId"] != version_id
+
+    # the destination key did not keep the legal hold from the source key
+    head_object = client.head_object(Bucket=bucket_name, Key=dest_key)
+    assert "ObjectLockLegalHoldStatus" not in head_object
+
+
+@mock_s3
+def test_s3_copy_object_lock():
+    client = boto3.client("s3", region_name=DEFAULT_REGION_NAME)
+    bucket_name = "testbucket"
+    source_key = "source-key"
+    dest_key = "dest-key"
+    client.create_bucket(Bucket=bucket_name, ObjectLockEnabledForBucket=True)
+    # manipulate a bit the datetime object for an easier comparison
+    retain_until = datetime.datetime.now(tz=datetime.timezone.utc) + datetime.timedelta(
+        minutes=1
+    )
+    retain_until = retain_until.replace(microsecond=0)
+
+    client.put_object(
+        Bucket=bucket_name,
+        Key=source_key,
+        Body="test",
+        ObjectLockMode="GOVERNANCE",
+        ObjectLockRetainUntilDate=retain_until,
+    )
+
+    head_object = client.head_object(Bucket=bucket_name, Key=source_key)
+
+    assert head_object["ObjectLockMode"] == "GOVERNANCE"
+    assert head_object["ObjectLockRetainUntilDate"] == retain_until
+    assert "VersionId" in head_object
+    version_id = head_object["VersionId"]
+
+    resp = client.copy_object(
+        Bucket=bucket_name,
+        CopySource=f"{bucket_name}/{source_key}",
+        Key=dest_key,
+    )
+    assert resp["CopySourceVersionId"] == version_id
+    assert resp["VersionId"] != version_id
+
+    # the destination key did not keep the lock mode nor the lock until from the source key
+    head_object = client.head_object(Bucket=bucket_name, Key=dest_key)
+    assert "ObjectLockMode" not in head_object
+    assert "ObjectLockRetainUntilDate" not in head_object
+
+
+@mock_s3
+def test_copy_object_in_place_website_redirect_location():
+    client = boto3.client("s3", region_name=DEFAULT_REGION_NAME)
+    bucket_name = "testbucket"
+    key = "source-key"
+    client.create_bucket(Bucket=bucket_name)
+    # this test will validate that setting WebsiteRedirectLocation (even the same as source) allows a copy in place
+
+    client.put_object(
+        Bucket=bucket_name,
+        Key=key,
+        Body="test",
+        WebsiteRedirectLocation="/test/direct",
+    )
+
+    head_object = client.head_object(Bucket=bucket_name, Key=key)
+    assert head_object["WebsiteRedirectLocation"] == "/test/direct"
+
+    # copy the object with the same WebsiteRedirectLocation as the source object
+    client.copy_object(
+        Bucket=bucket_name,
+        CopySource=f"{bucket_name}/{key}",
+        Key=key,
+        WebsiteRedirectLocation="/test/direct",
+    )
+
+    head_object = client.head_object(Bucket=bucket_name, Key=key)
+    assert head_object["WebsiteRedirectLocation"] == "/test/direct"
diff --git a/tests/test_scheduler/test_scheduler.py b/tests/test_scheduler/test_scheduler.py
index 5531a3b0f..399187eef 100644
--- a/tests/test_scheduler/test_scheduler.py
+++ b/tests/test_scheduler/test_scheduler.py
@@ -81,11 +81,22 @@ def test_create_get_delete__in_different_group():
     assert err["Code"] == "ResourceNotFoundException"
 
 
+@pytest.mark.parametrize(
+    "extra_kwargs",
+    [
+        ({}),
+        ({"GroupName": "some-group"}),
+    ],
+    ids=["without_group", "with_group"],
+)
 @mock_scheduler
-def test_update_schedule():
+def test_update_schedule(extra_kwargs):
     client = boto3.client("scheduler", region_name="eu-west-1")
 
+    client.create_schedule_group(Name="some-group")
+
     client.create_schedule(
+        **extra_kwargs,
         Name="my-schedule",
         ScheduleExpression="some cron",
         FlexibleTimeWindow={
@@ -99,6 +110,7 @@ def test_update_schedule():
     )
 
     client.update_schedule(
+        **extra_kwargs,
         Name="my-schedule",
         Description="new desc",
         ScheduleExpression="new cron",
@@ -113,7 +125,7 @@ def test_update_schedule():
         },
     )
 
-    schedule = client.get_schedule(Name="my-schedule")
+    schedule = client.get_schedule(**extra_kwargs, Name="my-schedule")
     assert schedule["Description"] == "new desc"
     assert schedule["ScheduleExpression"] == "new cron"
     assert schedule["State"] == "DISABLED"
diff --git a/tests/test_utilities/test_paginator.py b/tests/test_utilities/test_paginator.py
index 2ece17673..ed612e6a4 100644
--- a/tests/test_utilities/test_paginator.py
+++ b/tests/test_utilities/test_paginator.py
@@ -163,41 +163,41 @@ class TestDecorator(unittest.TestCase):
         "method_with_list_as_kwarg": {"limit_default": 1, "unique_attribute": "name"},
     }
 
-    @paginate(pagination_model=PAGINATION_MODEL)
+    @paginate(pagination_model=PAGINATION_MODEL)  # type: ignore[misc]
     def method_returning_dict(self):
         return results
 
-    @paginate(pagination_model=PAGINATION_MODEL)
+    @paginate(pagination_model=PAGINATION_MODEL)  # type: ignore[misc]
     def method_returning_instances(self):
         return model_results
 
-    @paginate(pagination_model=PAGINATION_MODEL)
+    @paginate(pagination_model=PAGINATION_MODEL)  # type: ignore[misc]
     def method_without_configuration(self):
         return results
 
-    @paginate(pagination_model=PAGINATION_MODEL)
+    @paginate(pagination_model=PAGINATION_MODEL)  # type: ignore[misc]
     def method_returning_args(self, *args, **kwargs):
         return [*args] + [(k, v) for k, v in kwargs.items()]
 
-    @paginate(pagination_model=PAGINATION_MODEL)
+    @paginate(pagination_model=PAGINATION_MODEL)  # type: ignore[misc]
     def method_expecting_token_as_kwarg(self, custom_token=None):
         self.custom_token = custom_token
         return [{"name": "item1"}, {"name": "item2"}]
 
-    @paginate(pagination_model=PAGINATION_MODEL)
+    @paginate(pagination_model=PAGINATION_MODEL)  # type: ignore[misc]
     def method_expecting_limit_as_kwarg(self, custom_limit):
         self.custom_limit = custom_limit
         return [{"name": "item1"}, {"name": "item2"}]
 
-    @paginate(pagination_model=PAGINATION_MODEL)
+    @paginate(pagination_model=PAGINATION_MODEL)  # type: ignore[misc]
     def method_with_list_as_kwarg(self, resources=[]):
         return resources or results
 
-    @paginate(PAGINATION_MODEL)
+    @paginate(PAGINATION_MODEL)  # type: ignore[misc]
     def method_specifying_invalidtoken_exception(self):
         return results
 
-    @paginate(PAGINATION_MODEL)
+    @paginate(PAGINATION_MODEL)  # type: ignore[misc]
     def method_specifying_generic_invalidtoken_exception(self):
         return results