Repository: getmoto/moto
Pull request #7249 (2024-01-25 00:44:46)

Support Route53 create_hosted_zone raising ClientError ConflictingDomainExists
Project does not support Route53 create_hosted_zone raising ConflictingDomainExists if one attempts to create a hosted zone with the same name on the same delegation set.  According to the docs, this should raise a ClientError named ConflictingDomainExists.

Simple Reproduction

```
@mock_route53
def test():
    conn = boto3.client('route53')
    conn.create_hosted_zone(Name='test.com', CallerReference='1', DelegationSetId='1')
    conn.create_hosted_zone(Name='test.com', CallerReference='2', DelegationSetId='1')
```

[Reference](https://docs.aws.amazon.com/sdk-for-ruby/v3/api/Aws/Route53/Types/ConflictingDomainExists.html)

Introduced test covers:
- Two create_hosted_zone requests on the same delegation set == should raise exception
- Two create_hosted_zone reqs with unnamed delegation set ids == should not raise
- Two create_hosted_zone reqs with different, named delegation set ids == should not raise

I read & followed the contribution guide, `make format` and tests pass but lmk if I missed anything!

bblommers: Hi @codeocelot!

There is `make lint` as well, for additional validation, and that currently fails: https://github.com/getmoto/moto/actions/runs/7648573846/job/20841579582?pr=7249

Note the `str | None` error in the linked GithubAction - this error will not show up if you're only testing a recent Python version, but it would not work for Python < 3.10.

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index f60799107..f9ade6ebd 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,6 +1,45 @@
 Moto Changelog
 ==============
 
+4.2.14
+-----
+Docker Digest for 4.2.14: <autopopulateddigest>
+
+    New Services:
+        * Backup:
+            * create_backup_plan()
+            * create_backup_vault()
+            * get_backup_plan()
+            * describe_backup_vault()
+            * delete_backup_plan()
+            * list_backup_plans()
+            * list_backup_vaults()
+            * list_tags()
+            * tag_resource()
+            * untag_resource()
+
+    New Methods:
+        * RDS:
+            * describe_db_cluster_snapshot_attributes()
+            * describe_db_snapshot_attributes()
+            * modify_db_cluster_snapshot_attribute()
+            * modify_db_snapshot_attribute()
+            * restore_db_instance_to_point_in_time()
+
+        * SageMaker:
+            * create_feature_group()
+
+        * SageMakerRuntime:
+            * invoke_endpoint_async()
+
+    Miscellaneous:
+        * Cognito: The ID-token now contains custom attributes
+        * DynamoDB: query() now returns the correct ScannedCount
+        * EC2: Security Group Rules now have tag support 
+        * LakeFormation: grant_permissions() now has better support for known principal-resource pairs
+        * SNS: set_subscription_attributes() can now unset the FilterPolicy
+
+
 4.2.13
 -----
 Docker Digest for 4.2.13: _sha256:20a2fdd4828b0ce1170ae26186ed28b64523cf6af83af892a74d9b3e23f84471_
diff --git a/IMPLEMENTATION_COVERAGE.md b/IMPLEMENTATION_COVERAGE.md
index c45abd1ba..04faa5101 100644
--- a/IMPLEMENTATION_COVERAGE.md
+++ b/IMPLEMENTATION_COVERAGE.md
@@ -575,6 +575,103 @@
 - [X] update_auto_scaling_group
 </details>
 
+## backup
+<details>
+<summary>9% implemented</summary>
+
+- [ ] cancel_legal_hold
+- [X] create_backup_plan
+- [ ] create_backup_selection
+- [X] create_backup_vault
+- [ ] create_framework
+- [ ] create_legal_hold
+- [ ] create_logically_air_gapped_backup_vault
+- [ ] create_report_plan
+- [ ] create_restore_testing_plan
+- [ ] create_restore_testing_selection
+- [X] delete_backup_plan
+- [ ] delete_backup_selection
+- [ ] delete_backup_vault
+- [ ] delete_backup_vault_access_policy
+- [ ] delete_backup_vault_lock_configuration
+- [ ] delete_backup_vault_notifications
+- [ ] delete_framework
+- [ ] delete_recovery_point
+- [ ] delete_report_plan
+- [ ] delete_restore_testing_plan
+- [ ] delete_restore_testing_selection
+- [ ] describe_backup_job
+- [ ] describe_backup_vault
+- [ ] describe_copy_job
+- [ ] describe_framework
+- [ ] describe_global_settings
+- [ ] describe_protected_resource
+- [ ] describe_recovery_point
+- [ ] describe_region_settings
+- [ ] describe_report_job
+- [ ] describe_report_plan
+- [ ] describe_restore_job
+- [ ] disassociate_recovery_point
+- [ ] disassociate_recovery_point_from_parent
+- [ ] export_backup_plan_template
+- [X] get_backup_plan
+- [ ] get_backup_plan_from_json
+- [ ] get_backup_plan_from_template
+- [ ] get_backup_selection
+- [ ] get_backup_vault_access_policy
+- [ ] get_backup_vault_notifications
+- [ ] get_legal_hold
+- [ ] get_recovery_point_restore_metadata
+- [ ] get_restore_job_metadata
+- [ ] get_restore_testing_inferred_metadata
+- [ ] get_restore_testing_plan
+- [ ] get_restore_testing_selection
+- [ ] get_supported_resource_types
+- [ ] list_backup_job_summaries
+- [ ] list_backup_jobs
+- [ ] list_backup_plan_templates
+- [ ] list_backup_plan_versions
+- [X] list_backup_plans
+- [ ] list_backup_selections
+- [X] list_backup_vaults
+- [ ] list_copy_job_summaries
+- [ ] list_copy_jobs
+- [ ] list_frameworks
+- [ ] list_legal_holds
+- [ ] list_protected_resources
+- [ ] list_protected_resources_by_backup_vault
+- [ ] list_recovery_points_by_backup_vault
+- [ ] list_recovery_points_by_legal_hold
+- [ ] list_recovery_points_by_resource
+- [ ] list_report_jobs
+- [ ] list_report_plans
+- [ ] list_restore_job_summaries
+- [ ] list_restore_jobs
+- [ ] list_restore_jobs_by_protected_resource
+- [ ] list_restore_testing_plans
+- [ ] list_restore_testing_selections
+- [X] list_tags
+- [ ] put_backup_vault_access_policy
+- [ ] put_backup_vault_lock_configuration
+- [ ] put_backup_vault_notifications
+- [ ] put_restore_validation_result
+- [ ] start_backup_job
+- [ ] start_copy_job
+- [ ] start_report_job
+- [ ] start_restore_job
+- [ ] stop_backup_job
+- [X] tag_resource
+- [X] untag_resource
+- [ ] update_backup_plan
+- [ ] update_framework
+- [ ] update_global_settings
+- [ ] update_recovery_point_lifecycle
+- [ ] update_region_settings
+- [ ] update_report_plan
+- [ ] update_restore_testing_plan
+- [ ] update_restore_testing_selection
+</details>
+
 ## batch
 <details>
 <summary>100% implemented</summary>
@@ -873,7 +970,7 @@
 
 ## cloudtrail
 <details>
-<summary>33% implemented</summary>
+<summary>32% implemented</summary>
 
 - [X] add_tags
 - [ ] cancel_query
@@ -902,6 +999,7 @@
 - [ ] list_event_data_stores
 - [ ] list_import_failures
 - [ ] list_imports
+- [ ] list_insights_metric_data
 - [ ] list_public_keys
 - [ ] list_queries
 - [X] list_tags
@@ -971,18 +1069,21 @@
 
 ## codebuild
 <details>
-<summary>17% implemented</summary>
+<summary>16% implemented</summary>
 
 - [ ] batch_delete_builds
 - [ ] batch_get_build_batches
 - [X] batch_get_builds
+- [ ] batch_get_fleets
 - [ ] batch_get_projects
 - [ ] batch_get_report_groups
 - [ ] batch_get_reports
+- [ ] create_fleet
 - [X] create_project
 - [ ] create_report_group
 - [ ] create_webhook
 - [ ] delete_build_batch
+- [ ] delete_fleet
 - [X] delete_project
 - [ ] delete_report
 - [ ] delete_report_group
@@ -1000,6 +1101,7 @@
 - [X] list_builds
 - [X] list_builds_for_project
 - [ ] list_curated_environment_images
+- [ ] list_fleets
 - [X] list_projects
 - [ ] list_report_groups
 - [ ] list_reports
@@ -1014,6 +1116,7 @@
 - [ ] start_build_batch
 - [X] stop_build
 - [ ] stop_build_batch
+- [ ] update_fleet
 - [ ] update_project
 - [ ] update_project_visibility
 - [ ] update_report_group
@@ -1832,7 +1935,7 @@
 
 ## dynamodb
 <details>
-<summary>60% implemented</summary>
+<summary>59% implemented</summary>
 
 - [X] batch_execute_statement
 - [X] batch_get_item
@@ -1884,6 +1987,7 @@
 - [ ] update_global_table
 - [ ] update_global_table_settings
 - [X] update_item
+- [ ] update_kinesis_streaming_destination
 - [X] update_table
 - [ ] update_table_replica_auto_scaling
 - [X] update_time_to_live
@@ -3928,7 +4032,7 @@
 
 ## inspector2
 <details>
-<summary>41% implemented</summary>
+<summary>32% implemented</summary>
 
 - [X] associate_member
 - [X] batch_get_account_status
@@ -3939,9 +4043,11 @@
 - [ ] batch_update_member_ec2_deep_inspection_status
 - [ ] cancel_findings_report
 - [ ] cancel_sbom_export
+- [ ] create_cis_scan_configuration
 - [X] create_filter
 - [ ] create_findings_report
 - [ ] create_sbom_export
+- [ ] delete_cis_scan_configuration
 - [X] delete_filter
 - [X] describe_organization_configuration
 - [X] disable
@@ -3949,6 +4055,8 @@
 - [X] disassociate_member
 - [X] enable
 - [X] enable_delegated_admin_account
+- [ ] get_cis_scan_report
+- [ ] get_cis_scan_result_details
 - [ ] get_configuration
 - [ ] get_delegated_admin_account
 - [ ] get_ec2_deep_inspection_configuration
@@ -3957,6 +4065,10 @@
 - [X] get_member
 - [ ] get_sbom_export
 - [ ] list_account_permissions
+- [ ] list_cis_scan_configurations
+- [ ] list_cis_scan_results_aggregated_by_checks
+- [ ] list_cis_scan_results_aggregated_by_target_resource
+- [ ] list_cis_scans
 - [ ] list_coverage
 - [ ] list_coverage_statistics
 - [X] list_delegated_admin_accounts
@@ -3968,8 +4080,13 @@
 - [ ] list_usage_totals
 - [ ] reset_encryption_key
 - [ ] search_vulnerabilities
+- [ ] send_cis_session_health
+- [ ] send_cis_session_telemetry
+- [ ] start_cis_session
+- [ ] stop_cis_session
 - [X] tag_resource
 - [X] untag_resource
+- [ ] update_cis_scan_configuration
 - [ ] update_configuration
 - [ ] update_ec2_deep_inspection_configuration
 - [ ] update_encryption_key
@@ -5663,7 +5780,7 @@
 
 ## rds
 <details>
-<summary>35% implemented</summary>
+<summary>37% implemented</summary>
 
 - [ ] add_role_to_db_cluster
 - [ ] add_role_to_db_instance
@@ -5690,6 +5807,7 @@
 - [ ] create_db_proxy
 - [ ] create_db_proxy_endpoint
 - [X] create_db_security_group
+- [ ] create_db_shard_group
 - [X] create_db_snapshot
 - [ ] create_db_subnet_group
 - [X] create_event_subscription
@@ -5710,6 +5828,7 @@
 - [ ] delete_db_proxy
 - [ ] delete_db_proxy_endpoint
 - [ ] delete_db_security_group
+- [ ] delete_db_shard_group
 - [X] delete_db_snapshot
 - [ ] delete_db_subnet_group
 - [X] delete_event_subscription
@@ -5726,7 +5845,7 @@
 - [ ] describe_db_cluster_endpoints
 - [X] describe_db_cluster_parameter_groups
 - [X] describe_db_cluster_parameters
-- [ ] describe_db_cluster_snapshot_attributes
+- [X] describe_db_cluster_snapshot_attributes
 - [X] describe_db_cluster_snapshots
 - [X] describe_db_clusters
 - [ ] describe_db_engine_versions
@@ -5741,7 +5860,8 @@
 - [ ] describe_db_proxy_targets
 - [ ] describe_db_recommendations
 - [ ] describe_db_security_groups
-- [ ] describe_db_snapshot_attributes
+- [ ] describe_db_shard_groups
+- [X] describe_db_snapshot_attributes
 - [ ] describe_db_snapshot_tenant_databases
 - [X] describe_db_snapshots
 - [X] describe_db_subnet_groups
@@ -5775,15 +5895,16 @@
 - [X] modify_db_cluster
 - [ ] modify_db_cluster_endpoint
 - [ ] modify_db_cluster_parameter_group
-- [ ] modify_db_cluster_snapshot_attribute
+- [X] modify_db_cluster_snapshot_attribute
 - [X] modify_db_instance
 - [X] modify_db_parameter_group
 - [ ] modify_db_proxy
 - [ ] modify_db_proxy_endpoint
 - [ ] modify_db_proxy_target_group
 - [ ] modify_db_recommendation
+- [ ] modify_db_shard_group
 - [ ] modify_db_snapshot
-- [ ] modify_db_snapshot_attribute
+- [X] modify_db_snapshot_attribute
 - [X] modify_db_subnet_group
 - [ ] modify_event_subscription
 - [ ] modify_global_cluster
@@ -5794,6 +5915,7 @@
 - [ ] purchase_reserved_db_instances_offering
 - [ ] reboot_db_cluster
 - [X] reboot_db_instance
+- [ ] reboot_db_shard_group
 - [ ] register_db_proxy_targets
 - [X] remove_from_global_cluster
 - [ ] remove_role_from_db_cluster
@@ -6526,7 +6648,7 @@
 
 ## sagemaker
 <details>
-<summary>19% implemented</summary>
+<summary>20% implemented</summary>
 
 - [ ] add_association
 - [X] add_tags
@@ -6552,7 +6674,7 @@
 - [X] create_endpoint
 - [X] create_endpoint_config
 - [X] create_experiment
-- [ ] create_feature_group
+- [X] create_feature_group
 - [ ] create_flow_definition
 - [ ] create_hub
 - [ ] create_human_task_ui
@@ -6611,6 +6733,7 @@
 - [ ] delete_hub
 - [ ] delete_hub_content
 - [ ] delete_human_task_ui
+- [ ] delete_hyper_parameter_tuning_job
 - [ ] delete_image
 - [ ] delete_image_version
 - [ ] delete_inference_component
@@ -6658,7 +6781,7 @@
 - [X] describe_endpoint
 - [X] describe_endpoint_config
 - [X] describe_experiment
-- [ ] describe_feature_group
+- [X] describe_feature_group
 - [ ] describe_feature_metadata
 - [ ] describe_flow_definition
 - [ ] describe_hub
@@ -6850,10 +6973,10 @@
 
 ## sagemaker-runtime
 <details>
-<summary>33% implemented</summary>
+<summary>66% implemented</summary>
 
 - [X] invoke_endpoint
-- [ ] invoke_endpoint_async
+- [X] invoke_endpoint_async
 - [ ] invoke_endpoint_with_response_stream
 </details>
 
@@ -7786,7 +7909,6 @@
 - auditmanager
 - autoscaling-plans
 - b2bi
-- backup
 - backup-gateway
 - backupstorage
 - bcm-data-exports
@@ -8000,6 +8122,7 @@
 - sso
 - sso-oidc
 - storagegateway
+- supplychain
 - support-app
 - synthetics
 - timestream-query
diff --git a/Makefile b/Makefile
index aa403f097..9c80b4c46 100644
--- a/Makefile
+++ b/Makefile
@@ -32,6 +32,7 @@ lint:
 
 format:
 	black moto/ tests/
+	ruff --fix moto/ tests/
 
 test-only:
 	rm -f .coverage
diff --git a/docs/docs/services/backup.rst b/docs/docs/services/backup.rst
new file mode 100644
index 000000000..a42837399
--- /dev/null
+++ b/docs/docs/services/backup.rst
@@ -0,0 +1,133 @@
+.. _implementedservice_backup:
+
+.. |start-h3| raw:: html
+
+    <h3>
+
+.. |end-h3| raw:: html
+
+    </h3>
+
+======
+backup
+======
+
+.. autoclass:: moto.backup.models.BackupBackend
+
+|start-h3| Example usage |end-h3|
+
+.. sourcecode:: python
+
+            @mock_backup
+            def test_backup_behaviour:
+                boto3.client("backup")
+                ...
+
+
+
+|start-h3| Implemented features for this service |end-h3|
+
+- [ ] cancel_legal_hold
+- [X] create_backup_plan
+- [ ] create_backup_selection
+- [X] create_backup_vault
+- [ ] create_framework
+- [ ] create_legal_hold
+- [ ] create_logically_air_gapped_backup_vault
+- [ ] create_report_plan
+- [ ] create_restore_testing_plan
+- [ ] create_restore_testing_selection
+- [X] delete_backup_plan
+- [ ] delete_backup_selection
+- [ ] delete_backup_vault
+- [ ] delete_backup_vault_access_policy
+- [ ] delete_backup_vault_lock_configuration
+- [ ] delete_backup_vault_notifications
+- [ ] delete_framework
+- [ ] delete_recovery_point
+- [ ] delete_report_plan
+- [ ] delete_restore_testing_plan
+- [ ] delete_restore_testing_selection
+- [ ] describe_backup_job
+- [ ] describe_backup_vault
+- [ ] describe_copy_job
+- [ ] describe_framework
+- [ ] describe_global_settings
+- [ ] describe_protected_resource
+- [ ] describe_recovery_point
+- [ ] describe_region_settings
+- [ ] describe_report_job
+- [ ] describe_report_plan
+- [ ] describe_restore_job
+- [ ] disassociate_recovery_point
+- [ ] disassociate_recovery_point_from_parent
+- [ ] export_backup_plan_template
+- [X] get_backup_plan
+- [ ] get_backup_plan_from_json
+- [ ] get_backup_plan_from_template
+- [ ] get_backup_selection
+- [ ] get_backup_vault_access_policy
+- [ ] get_backup_vault_notifications
+- [ ] get_legal_hold
+- [ ] get_recovery_point_restore_metadata
+- [ ] get_restore_job_metadata
+- [ ] get_restore_testing_inferred_metadata
+- [ ] get_restore_testing_plan
+- [ ] get_restore_testing_selection
+- [ ] get_supported_resource_types
+- [ ] list_backup_job_summaries
+- [ ] list_backup_jobs
+- [ ] list_backup_plan_templates
+- [ ] list_backup_plan_versions
+- [X] list_backup_plans
+  
+        Pagination is not yet implemented
+        
+
+- [ ] list_backup_selections
+- [X] list_backup_vaults
+  
+        Pagination is not yet implemented
+        
+
+- [ ] list_copy_job_summaries
+- [ ] list_copy_jobs
+- [ ] list_frameworks
+- [ ] list_legal_holds
+- [ ] list_protected_resources
+- [ ] list_protected_resources_by_backup_vault
+- [ ] list_recovery_points_by_backup_vault
+- [ ] list_recovery_points_by_legal_hold
+- [ ] list_recovery_points_by_resource
+- [ ] list_report_jobs
+- [ ] list_report_plans
+- [ ] list_restore_job_summaries
+- [ ] list_restore_jobs
+- [ ] list_restore_jobs_by_protected_resource
+- [ ] list_restore_testing_plans
+- [ ] list_restore_testing_selections
+- [X] list_tags
+  
+        Pagination is not yet implemented
+        
+
+- [ ] put_backup_vault_access_policy
+- [ ] put_backup_vault_lock_configuration
+- [ ] put_backup_vault_notifications
+- [ ] put_restore_validation_result
+- [ ] start_backup_job
+- [ ] start_copy_job
+- [ ] start_report_job
+- [ ] start_restore_job
+- [ ] stop_backup_job
+- [X] tag_resource
+- [X] untag_resource
+- [ ] update_backup_plan
+- [ ] update_framework
+- [ ] update_global_settings
+- [ ] update_recovery_point_lifecycle
+- [ ] update_region_settings
+- [ ] update_report_plan
+- [ ] update_restore_testing_plan
+- [ ] update_restore_testing_selection
+
diff --git a/docs/docs/services/cloudtrail.rst b/docs/docs/services/cloudtrail.rst
index ffd52063f..72134381e 100644
--- a/docs/docs/services/cloudtrail.rst
+++ b/docs/docs/services/cloudtrail.rst
@@ -54,6 +54,7 @@ cloudtrail
 - [ ] list_event_data_stores
 - [ ] list_import_failures
 - [ ] list_imports
+- [ ] list_insights_metric_data
 - [ ] list_public_keys
 - [ ] list_queries
 - [X] list_tags
diff --git a/docs/docs/services/codebuild.rst b/docs/docs/services/codebuild.rst
index bab3d904d..a0aaa07ed 100644
--- a/docs/docs/services/codebuild.rst
+++ b/docs/docs/services/codebuild.rst
@@ -28,13 +28,16 @@ codebuild
 - [ ] batch_delete_builds
 - [ ] batch_get_build_batches
 - [X] batch_get_builds
+- [ ] batch_get_fleets
 - [ ] batch_get_projects
 - [ ] batch_get_report_groups
 - [ ] batch_get_reports
+- [ ] create_fleet
 - [X] create_project
 - [ ] create_report_group
 - [ ] create_webhook
 - [ ] delete_build_batch
+- [ ] delete_fleet
 - [X] delete_project
 - [ ] delete_report
 - [ ] delete_report_group
@@ -52,6 +55,7 @@ codebuild
 - [X] list_builds
 - [X] list_builds_for_project
 - [ ] list_curated_environment_images
+- [ ] list_fleets
 - [X] list_projects
 - [ ] list_report_groups
 - [ ] list_reports
@@ -66,6 +70,7 @@ codebuild
 - [ ] start_build_batch
 - [X] stop_build
 - [ ] stop_build_batch
+- [ ] update_fleet
 - [ ] update_project
 - [ ] update_project_visibility
 - [ ] update_report_group
diff --git a/docs/docs/services/dynamodb.rst b/docs/docs/services/dynamodb.rst
index 02ab61c11..fc37b3e28 100644
--- a/docs/docs/services/dynamodb.rst
+++ b/docs/docs/services/dynamodb.rst
@@ -94,6 +94,7 @@ dynamodb
 - [ ] update_global_table
 - [ ] update_global_table_settings
 - [X] update_item
+- [ ] update_kinesis_streaming_destination
 - [X] update_table
 - [ ] update_table_replica_auto_scaling
 - [X] update_time_to_live
diff --git a/docs/docs/services/inspector2.rst b/docs/docs/services/inspector2.rst
index 1cb6fe3ac..8c8d39e8e 100644
--- a/docs/docs/services/inspector2.rst
+++ b/docs/docs/services/inspector2.rst
@@ -34,9 +34,11 @@ inspector2
 - [ ] batch_update_member_ec2_deep_inspection_status
 - [ ] cancel_findings_report
 - [ ] cancel_sbom_export
+- [ ] create_cis_scan_configuration
 - [X] create_filter
 - [ ] create_findings_report
 - [ ] create_sbom_export
+- [ ] delete_cis_scan_configuration
 - [X] delete_filter
 - [X] describe_organization_configuration
 - [X] disable
@@ -44,6 +46,8 @@ inspector2
 - [X] disassociate_member
 - [X] enable
 - [X] enable_delegated_admin_account
+- [ ] get_cis_scan_report
+- [ ] get_cis_scan_result_details
 - [ ] get_configuration
 - [ ] get_delegated_admin_account
 - [ ] get_ec2_deep_inspection_configuration
@@ -52,6 +56,10 @@ inspector2
 - [X] get_member
 - [ ] get_sbom_export
 - [ ] list_account_permissions
+- [ ] list_cis_scan_configurations
+- [ ] list_cis_scan_results_aggregated_by_checks
+- [ ] list_cis_scan_results_aggregated_by_target_resource
+- [ ] list_cis_scans
 - [ ] list_coverage
 - [ ] list_coverage_statistics
 - [X] list_delegated_admin_accounts
@@ -99,8 +107,13 @@ inspector2
 - [ ] list_usage_totals
 - [ ] reset_encryption_key
 - [ ] search_vulnerabilities
+- [ ] send_cis_session_health
+- [ ] send_cis_session_telemetry
+- [ ] start_cis_session
+- [ ] stop_cis_session
 - [X] tag_resource
 - [X] untag_resource
+- [ ] update_cis_scan_configuration
 - [ ] update_configuration
 - [ ] update_ec2_deep_inspection_configuration
 - [ ] update_encryption_key
diff --git a/docs/docs/services/rds.rst b/docs/docs/services/rds.rst
index 6542a8319..6eb5e48ae 100644
--- a/docs/docs/services/rds.rst
+++ b/docs/docs/services/rds.rst
@@ -50,6 +50,7 @@ rds
 - [ ] create_db_proxy
 - [ ] create_db_proxy_endpoint
 - [X] create_db_security_group
+- [ ] create_db_shard_group
 - [X] create_db_snapshot
 - [ ] create_db_subnet_group
 - [X] create_event_subscription
@@ -70,6 +71,7 @@ rds
 - [ ] delete_db_proxy
 - [ ] delete_db_proxy_endpoint
 - [ ] delete_db_security_group
+- [ ] delete_db_shard_group
 - [X] delete_db_snapshot
 - [ ] delete_db_subnet_group
 - [X] delete_event_subscription
@@ -86,7 +88,7 @@ rds
 - [ ] describe_db_cluster_endpoints
 - [X] describe_db_cluster_parameter_groups
 - [X] describe_db_cluster_parameters
-- [ ] describe_db_cluster_snapshot_attributes
+- [X] describe_db_cluster_snapshot_attributes
 - [X] describe_db_cluster_snapshots
 - [X] describe_db_clusters
 - [ ] describe_db_engine_versions
@@ -101,7 +103,8 @@ rds
 - [ ] describe_db_proxy_targets
 - [ ] describe_db_recommendations
 - [ ] describe_db_security_groups
-- [ ] describe_db_snapshot_attributes
+- [ ] describe_db_shard_groups
+- [X] describe_db_snapshot_attributes
 - [ ] describe_db_snapshot_tenant_databases
 - [X] describe_db_snapshots
 - [X] describe_db_subnet_groups
@@ -139,15 +142,16 @@ rds
 - [X] modify_db_cluster
 - [ ] modify_db_cluster_endpoint
 - [ ] modify_db_cluster_parameter_group
-- [ ] modify_db_cluster_snapshot_attribute
+- [X] modify_db_cluster_snapshot_attribute
 - [X] modify_db_instance
 - [X] modify_db_parameter_group
 - [ ] modify_db_proxy
 - [ ] modify_db_proxy_endpoint
 - [ ] modify_db_proxy_target_group
 - [ ] modify_db_recommendation
+- [ ] modify_db_shard_group
 - [ ] modify_db_snapshot
-- [ ] modify_db_snapshot_attribute
+- [X] modify_db_snapshot_attribute
 - [X] modify_db_subnet_group
 - [ ] modify_event_subscription
 - [ ] modify_global_cluster
@@ -158,6 +162,7 @@ rds
 - [ ] purchase_reserved_db_instances_offering
 - [ ] reboot_db_cluster
 - [X] reboot_db_instance
+- [ ] reboot_db_shard_group
 - [ ] register_db_proxy_targets
 - [X] remove_from_global_cluster
 - [ ] remove_role_from_db_cluster
@@ -171,7 +176,7 @@ rds
 - [ ] restore_db_cluster_to_point_in_time
 - [X] restore_db_instance_from_db_snapshot
 - [ ] restore_db_instance_from_s3
-- [ ] restore_db_instance_to_point_in_time
+- [X] restore_db_instance_to_point_in_time
 - [ ] revoke_db_security_group_ingress
 - [ ] start_activity_stream
 - [X] start_db_cluster
diff --git a/docs/docs/services/sagemaker-runtime.rst b/docs/docs/services/sagemaker-runtime.rst
index fdeaf11f5..8c9352f52 100644
--- a/docs/docs/services/sagemaker-runtime.rst
+++ b/docs/docs/services/sagemaker-runtime.rst
@@ -62,6 +62,6 @@ sagemaker-runtime
 
         
 
-- [ ] invoke_endpoint_async
+- [X] invoke_endpoint_async
 - [ ] invoke_endpoint_with_response_stream
 
diff --git a/docs/docs/services/sagemaker.rst b/docs/docs/services/sagemaker.rst
index 87b2d2827..50a86186f 100644
--- a/docs/docs/services/sagemaker.rst
+++ b/docs/docs/services/sagemaker.rst
@@ -49,7 +49,7 @@ sagemaker
 - [X] create_endpoint
 - [X] create_endpoint_config
 - [X] create_experiment
-- [ ] create_feature_group
+- [X] create_feature_group
 - [ ] create_flow_definition
 - [ ] create_hub
 - [ ] create_human_task_ui
@@ -108,6 +108,7 @@ sagemaker
 - [ ] delete_hub
 - [ ] delete_hub_content
 - [ ] delete_human_task_ui
+- [ ] delete_hyper_parameter_tuning_job
 - [ ] delete_image
 - [ ] delete_image_version
 - [ ] delete_inference_component
@@ -155,7 +156,7 @@ sagemaker
 - [X] describe_endpoint
 - [X] describe_endpoint_config
 - [X] describe_experiment
-- [ ] describe_feature_group
+- [X] describe_feature_group
 - [ ] describe_feature_metadata
 - [ ] describe_flow_definition
 - [ ] describe_hub
diff --git a/moto/__init__.py b/moto/__init__.py
index 72865ad29..7c1b02e69 100644
--- a/moto/__init__.py
+++ b/moto/__init__.py
@@ -235,6 +235,7 @@ XRaySegment = load_xray_segment()
 mock_xray = lazy_load(".xray", "mock_xray")
 mock_xray_client = lazy_load(".xray", "mock_xray_client")
 mock_wafv2 = lazy_load(".wafv2", "mock_wafv2")
+mock_backup = lazy_load(".backup", "mock_backup", boto3_name="backup")
 
 
 class MockAll(ContextDecorator):
diff --git a/moto/backend_index.py b/moto/backend_index.py
index 300784891..91c8e707a 100644
--- a/moto/backend_index.py
+++ b/moto/backend_index.py
@@ -1,4 +1,4 @@
-# autogenerated by moto/scripts/update_backend_index.py
+# autogenerated by /Users/oxh252/workspace/moto_contribution/moto/scripts/update_backend_index.py
 import re
 
 backend_url_patterns = [
@@ -18,6 +18,7 @@ backend_url_patterns = [
     ("appsync", re.compile("https?://appsync\\.(.+)\\.amazonaws\\.com")),
     ("athena", re.compile("https?://athena\\.(.+)\\.amazonaws\\.com")),
     ("autoscaling", re.compile("https?://autoscaling\\.(.+)\\.amazonaws\\.com")),
+    ("backup", re.compile("https?://backup\\.(.+)\\.amazonaws\\.com")),
     ("batch", re.compile("https?://batch\\.(.+)\\.amazonaws.com")),
     ("budgets", re.compile("https?://budgets\\.amazonaws\\.com")),
     ("ce", re.compile("https?://ce\\.(.+)\\.amazonaws\\.com")),
diff --git a/moto/backup/__init__.py b/moto/backup/__init__.py
new file mode 100644
index 000000000..092556a72
--- /dev/null
+++ b/moto/backup/__init__.py
@@ -0,0 +1,5 @@
+"""backup module initialization; sets value for base decorator."""
+from ..core.models import base_decorator
+from .models import backup_backends
+
+mock_backup = base_decorator(backup_backends)
diff --git a/moto/backup/exceptions.py b/moto/backup/exceptions.py
new file mode 100644
index 000000000..dc0e1ad12
--- /dev/null
+++ b/moto/backup/exceptions.py
@@ -0,0 +1,16 @@
+"""Exceptions raised by the backup service."""
+from moto.core.exceptions import JsonRESTError
+
+
+class BackupClientError(JsonRESTError):
+    code = 400
+
+
+class AlreadyExistsException(BackupClientError):
+    def __init__(self, msg: str):
+        super().__init__("AlreadyExistsException", f"{msg}")
+
+
+class ResourceNotFoundException(JsonRESTError):
+    def __init__(self, msg: str):
+        super().__init__("ResourceNotFoundException", f"{msg}")
diff --git a/moto/backup/models.py b/moto/backup/models.py
new file mode 100644
index 000000000..f2f040aed
--- /dev/null
+++ b/moto/backup/models.py
@@ -0,0 +1,236 @@
+"""BackupBackend class with methods for supported APIs."""
+from copy import deepcopy
+from typing import Any, Dict, List, Optional, Tuple
+
+from moto.core import BackendDict, BaseBackend, BaseModel
+from moto.core.utils import unix_time
+from moto.moto_api._internal import mock_random
+from moto.utilities.tagging_service import TaggingService
+
+from .exceptions import AlreadyExistsException, ResourceNotFoundException
+
+
+class Plan(BaseModel):
+    def __init__(
+        self,
+        backup_plan: Dict[str, Any],
+        creator_request_id: str,
+        backend: "BackupBackend",
+    ):
+
+        self.backup_plan_id = str(mock_random.uuid4())
+        self.backup_plan_arn = f"arn:aws:backup:{backend.region_name}:{backend.account_id}:backup-plan:{self.backup_plan_id}"
+        self.creation_date = unix_time()
+        ran_str = mock_random.get_random_string(length=48)
+        self.version_id = ran_str
+        self.creator_request_id = creator_request_id
+        self.backup_plan = backup_plan
+        adv_settings = backup_plan.get("AdvancedBackupSettings")
+        self.advanced_backup_settings = adv_settings or []
+        self.deletion_date: Optional[float] = None
+        # Deletion Date is updated when the backup_plan is deleted
+        self.last_execution_date = None  # start_restore_job not yet supported
+        rules = backup_plan["Rules"]
+        for rule in rules:
+            rule["ScheduleExpression"] = rule.get(
+                "ScheduleExpression", "cron(0 5 ? * * *)"
+            )  # Default CRON expression in UTC
+            rule["StartWindowMinutes"] = rule.get(
+                "StartWindowMinutes", 480
+            )  # Default=480
+            rule["CompletionWindowMinutes"] = rule.get(
+                "CompletionWindowMinutes", 10080
+            )  # Default=10080
+            rule["ScheduleExpressionTimezone"] = rule.get(
+                "ScheduleExpressionTimezone", "Etc/UTC"
+            )  # set to Etc/UTc by default
+            rule["RuleId"] = str(mock_random.uuid4())
+
+    def to_dict(self) -> Dict[str, Any]:
+        dct = {
+            "BackupPlanId": self.backup_plan_id,
+            "BackupPlanArn": self.backup_plan_arn,
+            "CreationDate": self.creation_date,
+            "VersionId": self.version_id,
+            "AdvancedBackupSettings": self.advanced_backup_settings,
+        }
+        return {k: v for k, v in dct.items() if v}
+
+    def to_get_dict(self) -> Dict[str, Any]:
+        dct = self.to_dict()
+        dct_options = {
+            "BackupPlan": self.backup_plan,
+            "CreatorRequestId": self.creator_request_id,
+            "DeletionDate": self.deletion_date,
+            "LastExecutionDate": self.last_execution_date,
+        }
+        for key, value in dct_options.items():
+            if value is not None:
+                dct[key] = value
+        return dct
+
+    def to_list_dict(self) -> Dict[str, Any]:
+        dct = self.to_get_dict()
+        dct.pop("BackupPlan")
+        dct["BackupPlanName"] = self.backup_plan.get("BackupPlanName")
+        return dct
+
+
+class Vault(BaseModel):
+    def __init__(
+        self,
+        backup_vault_name: str,
+        encryption_key_arn: str,
+        creator_request_id: str,
+        backend: "BackupBackend",
+    ):
+        self.backup_vault_name = backup_vault_name
+        self.backup_vault_arn = f"arn:aws:backup:{backend.region_name}:{backend.account_id}:backup-vault:{backup_vault_name}"
+        self.creation_date = unix_time()
+        self.encryption_key_arn = encryption_key_arn
+        self.creator_request_id = creator_request_id
+        self.num_of_recovery_points = 0  # start_backup_job not yet supported
+        self.locked = False  # put_backup_vault_lock_configuration
+        self.min_retention_days = 0  # put_backup_vault_lock_configuration
+        self.max_retention_days = 0  # put_backup_vault_lock_configuration
+        self.lock_date = None  # put_backup_vault_lock_configuration
+
+    def to_dict(self) -> Dict[str, Any]:
+        dct = {
+            "BackupVaultName": self.backup_vault_name,
+            "BackupVaultArn": self.backup_vault_arn,
+            "CreationDate": self.creation_date,
+        }
+        return dct
+
+    def to_list_dict(self) -> Dict[str, Any]:
+        dct = self.to_dict()
+        dct_options: Dict[str, Any] = dict()
+        dct_options = {
+            "EncryptionKeyArn": self.encryption_key_arn,
+            "CreatorRequestId": self.creator_request_id,
+            "NumberOfRecoveryPoints": self.num_of_recovery_points,
+            "Locked": self.locked,
+            "MinRetentionDays": self.min_retention_days,
+            "MaxRetentionDays": self.max_retention_days,
+            "LockDate": self.lock_date,
+        }
+        for key, value in dct_options.items():
+            if value is not None:
+                dct[key] = value
+        return dct
+
+
+class BackupBackend(BaseBackend):
+    """Implementation of Backup APIs."""
+
+    def __init__(self, region_name: str, account_id: str):
+        super().__init__(region_name, account_id)
+
+        self.vaults: Dict[str, Vault] = dict()
+        self.plans: Dict[str, Plan] = dict()
+        self.tagger = TaggingService()
+
+    def create_backup_plan(
+        self,
+        backup_plan: Dict[str, Any],
+        backup_plan_tags: Dict[str, str],
+        creator_request_id: str,
+    ) -> Plan:
+
+        if backup_plan["BackupPlanName"] in list(
+            p.backup_plan["BackupPlanName"] for p in list(self.plans.values())
+        ):
+            raise AlreadyExistsException(
+                msg="Backup plan with the same plan document already exists"
+            )
+        plan = Plan(
+            backup_plan=backup_plan,
+            creator_request_id=creator_request_id,
+            backend=self,
+        )
+        if backup_plan_tags:
+            self.tag_resource(plan.backup_plan_arn, backup_plan_tags)
+        self.plans[plan.backup_plan_id] = plan
+        return plan
+
+    def get_backup_plan(self, backup_plan_id: str, version_id: Optional[Any]) -> Plan:
+        msg = "Failed reading Backup plan with provided version"
+        if backup_plan_id not in self.plans:
+            raise ResourceNotFoundException(msg=msg)
+        plan = self.plans[backup_plan_id]
+        if version_id:
+            if plan.version_id == version_id:
+                return plan
+            else:
+                raise ResourceNotFoundException(msg=msg)
+        return plan
+
+    def delete_backup_plan(self, backup_plan_id: str) -> Tuple[str, str, float, str]:
+        if backup_plan_id not in self.plans:
+            raise ResourceNotFoundException(
+                msg="Failed reading Backup plan with provided version"
+            )
+        deletion_date = unix_time()
+        res = self.plans[backup_plan_id]
+        res.deletion_date = deletion_date
+        return res.backup_plan_id, res.backup_plan_arn, deletion_date, res.version_id
+
+    def list_backup_plans(self, include_deleted: Any) -> List[Plan]:
+        """
+        Pagination is not yet implemented
+        """
+        plans_list = deepcopy(self.plans)
+
+        for plan in list(plans_list.values()):
+            backup_plan_id = plan.backup_plan_id
+            if plan.deletion_date is not None:
+                plans_list.pop(backup_plan_id)
+        if include_deleted:
+            return list(self.plans.values())
+        return list(plans_list.values())
+
+    def create_backup_vault(
+        self,
+        backup_vault_name: str,
+        backup_vault_tags: Dict[str, str],
+        encryption_key_arn: str,
+        creator_request_id: str,
+    ) -> Vault:
+
+        if backup_vault_name in self.vaults:
+            raise AlreadyExistsException(
+                msg="Backup vault with the same name already exists"
+            )
+        vault = Vault(
+            backup_vault_name=backup_vault_name,
+            encryption_key_arn=encryption_key_arn,
+            creator_request_id=creator_request_id,
+            backend=self,
+        )
+        if backup_vault_tags:
+            self.tag_resource(vault.backup_vault_arn, backup_vault_tags)
+        self.vaults[backup_vault_name] = vault
+        return vault
+
+    def list_backup_vaults(self) -> List[Vault]:
+        """
+        Pagination is not yet implemented
+        """
+        return list(self.vaults.values())
+
+    def list_tags(self, resource_arn: str) -> Dict[str, str]:
+        """
+        Pagination is not yet implemented
+        """
+        return self.tagger.get_tag_dict_for_resource(resource_arn)
+
+    def tag_resource(self, resource_arn: str, tags: Dict[str, str]) -> None:
+        tags_input = TaggingService.convert_dict_to_tags_input(tags or {})
+        self.tagger.tag_resource(resource_arn, tags_input)
+
+    def untag_resource(self, resource_arn: str, tag_key_list: List[str]) -> None:
+        self.tagger.untag_resource_using_names(resource_arn, tag_key_list)
+
+
+backup_backends = BackendDict(BackupBackend, "backup")
diff --git a/moto/backup/responses.py b/moto/backup/responses.py
new file mode 100644
index 000000000..42483740b
--- /dev/null
+++ b/moto/backup/responses.py
@@ -0,0 +1,116 @@
+"""Handles incoming backup requests, invokes methods, returns responses."""
+import json
+from urllib.parse import unquote
+
+from moto.core.responses import BaseResponse
+
+from .models import BackupBackend, backup_backends
+
+
+class BackupResponse(BaseResponse):
+    """Handler for Backup requests and responses."""
+
+    def __init__(self) -> None:
+        super().__init__(service_name="backup")
+
+    @property
+    def backup_backend(self) -> BackupBackend:
+        """Return backend instance specific for this region."""
+        return backup_backends[self.current_account][self.region]
+
+    def create_backup_plan(self) -> str:
+        params = json.loads(self.body)
+        backup_plan = params.get("BackupPlan")
+        backup_plan_tags = params.get("BackupPlanTags")
+        creator_request_id = params.get("CreatorRequestId")
+        plan = self.backup_backend.create_backup_plan(
+            backup_plan=backup_plan,
+            backup_plan_tags=backup_plan_tags,
+            creator_request_id=creator_request_id,
+        )
+        return json.dumps(dict(plan.to_dict()))
+
+    def get_backup_plan(self) -> str:
+        params = self._get_params()
+        backup_plan_id = self.path.split("/")[-2]
+        version_id = params.get("versionId")
+        plan = self.backup_backend.get_backup_plan(
+            backup_plan_id=backup_plan_id, version_id=version_id
+        )
+        return json.dumps(dict(plan.to_get_dict()))
+
+    def delete_backup_plan(self) -> str:
+        backup_plan_id = self.path.split("/")[-1]
+        (
+            backup_plan_id,
+            backup_plan_arn,
+            deletion_date,
+            version_id,
+        ) = self.backup_backend.delete_backup_plan(
+            backup_plan_id=backup_plan_id,
+        )
+        return json.dumps(
+            dict(
+                BackupPlanId=backup_plan_id,
+                BackupPlanArn=backup_plan_arn,
+                DeletionDate=deletion_date,
+                VersionId=version_id,
+            )
+        )
+
+    def list_backup_plans(self) -> str:
+        params = self._get_params()
+        include_deleted = params.get("includeDeleted")
+        backup_plans_list = self.backup_backend.list_backup_plans(
+            include_deleted=include_deleted
+        )
+        return json.dumps(
+            dict(BackupPlansList=[p.to_list_dict() for p in backup_plans_list])
+        )
+
+    def create_backup_vault(self) -> str:
+        params = json.loads(self.body)
+        backup_vault_name = self.path.split("/")[-1]
+        backup_vault_tags = params.get("BackupVaultTags")
+        encryption_key_arn = params.get("EncryptionKeyArn")
+        creator_request_id = params.get("CreatorRequestId")
+        backup_vault = self.backup_backend.create_backup_vault(
+            backup_vault_name=backup_vault_name,
+            backup_vault_tags=backup_vault_tags,
+            encryption_key_arn=encryption_key_arn,
+            creator_request_id=creator_request_id,
+        )
+        return json.dumps(dict(backup_vault.to_dict()))
+
+    def list_backup_vaults(self) -> str:
+        backup_vault_list = self.backup_backend.list_backup_vaults()
+        return json.dumps(
+            dict(BackupVaultList=[v.to_list_dict() for v in backup_vault_list])
+        )
+
+    def list_tags(self) -> str:
+        resource_arn = unquote(self.path.split("/")[-2])
+        tags = self.backup_backend.list_tags(
+            resource_arn=resource_arn,
+        )
+        return json.dumps(dict(Tags=tags))
+
+    def tag_resource(self) -> str:
+        params = json.loads(self.body)
+        resource_arn = unquote(self.path.split("/")[-1])
+        tags = params.get("Tags")
+        self.backup_backend.tag_resource(
+            resource_arn=resource_arn,
+            tags=tags,
+        )
+        return "{}"
+
+    def untag_resource(self) -> str:
+        params = json.loads(self.body)
+        resource_arn = unquote(self.path.split("/")[-1])
+        tag_key_list = params.get("TagKeyList")
+        self.backup_backend.untag_resource(
+            resource_arn=resource_arn,
+            tag_key_list=tag_key_list,
+        )
+        return "{}"
diff --git a/moto/backup/urls.py b/moto/backup/urls.py
new file mode 100644
index 000000000..5b4b7c84b
--- /dev/null
+++ b/moto/backup/urls.py
@@ -0,0 +1,19 @@
+"""backup base URL and path."""
+from .responses import BackupResponse
+
+url_bases = [
+    r"https?://backup\.(.+)\.amazonaws\.com",
+]
+
+
+response = BackupResponse()
+
+
+url_paths = {
+    "{0}/backup/plans/?$": response.dispatch,
+    "{0}/backup/plans/(?P<name>.+)/?$": response.dispatch,
+    "{0}/backup-vaults/$": response.dispatch,
+    "{0}/backup-vaults/(?P<name>[^/]+)$": response.dispatch,
+    "{0}/tags/(?P<resource_arn>.+)$": response.dispatch,
+    "{0}/untag/(?P<resource_arn>.+)$": response.dispatch,
+}
diff --git a/moto/core/utils.py b/moto/core/utils.py
index 8f3ddfce0..8e817dda2 100644
--- a/moto/core/utils.py
+++ b/moto/core/utils.py
@@ -170,10 +170,36 @@ def iso_8601_datetime_without_milliseconds_s3(
 
 
 RFC1123 = "%a, %d %b %Y %H:%M:%S GMT"
+EN_WEEKDAYS = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]
+EN_MONTHS = [
+    "Jan",
+    "Feb",
+    "Mar",
+    "Apr",
+    "May",
+    "Jun",
+    "Jul",
+    "Aug",
+    "Sep",
+    "Oct",
+    "Nov",
+    "Dec",
+]
 
 
 def rfc_1123_datetime(src: datetime.datetime) -> str:
-    return src.strftime(RFC1123)
+    """
+    Returns the datetime in the RFC-1123 format
+    Names of weekdays/months are in English
+    """
+    # strftime uses the current locale to translate values
+    # So weekday/month values may not be in English
+    # For our usecase, we need these values to always be in English, otherwise botocore is unable to parse it
+    # Having a hardcoded list ensures this always works, even if the user does not have an English locale installed
+    eng_weekday = EN_WEEKDAYS[src.isoweekday() - 1]
+    eng_month = EN_MONTHS[src.month - 1]
+    non_locallized_rfc1123 = RFC1123.replace("%a", "{}").replace("%b", "{}")
+    return src.strftime(non_locallized_rfc1123).format(eng_weekday, eng_month)
 
 
 def str_to_rfc_1123_datetime(value: str) -> datetime.datetime:
diff --git a/moto/rds/models.py b/moto/rds/models.py
index 1ac46d05a..1237d167e 100644
--- a/moto/rds/models.py
+++ b/moto/rds/models.py
@@ -1818,7 +1818,13 @@ class RDSBackend(BaseBackend):
         db_instance = self.describe_db_instances(
             db_instance_identifier=source_db_identifier
         )[0]
-        new_instance_props = copy.deepcopy(db_instance.__dict__)
+
+        # remove the db subnet group as it cannot be copied
+        # and is not used in the restored instance
+        source_dict = db_instance.__dict__
+        del source_dict["db_subnet_group"]
+
+        new_instance_props = copy.deepcopy(source_dict)
         if not db_instance.option_group_supplied:
             # If the option group is not supplied originally, the 'option_group_name' will receive a default value
             # Force this reconstruction, and prevent any validation on the default value
diff --git a/moto/resourcegroupstaggingapi/models.py b/moto/resourcegroupstaggingapi/models.py
index 41219e4d5..90c7a76ab 100644
--- a/moto/resourcegroupstaggingapi/models.py
+++ b/moto/resourcegroupstaggingapi/models.py
@@ -2,6 +2,7 @@ from typing import Any, Dict, Iterator, List, Optional, Tuple
 
 from moto.acm.models import AWSCertificateManagerBackend, acm_backends
 from moto.awslambda.models import LambdaBackend, lambda_backends
+from moto.backup.models import BackupBackend, backup_backends
 from moto.core import BackendDict, BaseBackend
 from moto.core.exceptions import RESTError
 from moto.ec2 import ec2_backends
@@ -100,6 +101,10 @@ class ResourceGroupsTaggingAPIBackend(BaseBackend):
     def sqs_backend(self) -> SQSBackend:
         return sqs_backends[self.account_id][self.region_name]
 
+    @property
+    def backup_backend(self) -> BackupBackend:
+        return backup_backends[self.account_id][self.region_name]
+
     def _get_resources_generator(
         self,
         tag_filters: Optional[List[Dict[str, Any]]] = None,
@@ -165,6 +170,19 @@ class ResourceGroupsTaggingAPIBackend(BaseBackend):
                     continue
                 yield {"ResourceARN": f"{certificate.arn}", "Tags": tags}
 
+        # Backup
+        if not resource_type_filters or "backup" in resource_type_filters:
+            for vault in self.backup_backend.vaults.values():
+                tags = self.backup_backend.tagger.list_tags_for_resource(
+                    vault.backup_vault_arn
+                )["Tags"]
+                if not tags or not tag_filter(
+                    tags
+                ):  # Skip if no tags, or invalid filter
+                    continue
+
+                yield {"ResourceARN": f"{vault.backup_vault_arn}", "Tags": tags}
+
         # S3
         if not resource_type_filters or "s3" in resource_type_filters:
             for bucket in self.s3_backend.buckets.values():
diff --git a/moto/route53/exceptions.py b/moto/route53/exceptions.py
index c5d5d7459..9d236459a 100644
--- a/moto/route53/exceptions.py
+++ b/moto/route53/exceptions.py
@@ -1,5 +1,5 @@
 """Exceptions raised by the Route53 service."""
-from typing import Any
+from typing import Any, Optional
 
 from moto.core.exceptions import RESTError
 
@@ -12,6 +12,19 @@ class Route53ClientError(RESTError):
         super().__init__(*args, **kwargs)
 
 
+class ConflictingDomainExists(Route53ClientError):
+    """Domain already exists."""
+
+    code = 400
+
+    def __init__(self, domain_name: str, delegation_set_id: Optional[str]) -> None:
+        message = (
+            f"Cannot create hosted zone with DelegationSetId DelegationSetId:{delegation_set_id} as the DNSName"
+            f"{domain_name} conflicts with existing ones sharing the delegation set"
+        )
+        super().__init__("ConflictingDomainExists", message)
+
+
 class InvalidInput(Route53ClientError):
     """Malformed ARN for the CloudWatch log group."""
 
diff --git a/moto/route53/models.py b/moto/route53/models.py
index ccfd5dc4d..195056cb0 100644
--- a/moto/route53/models.py
+++ b/moto/route53/models.py
@@ -12,6 +12,7 @@ from jinja2 import Template
 from moto.core import BackendDict, BaseBackend, BaseModel, CloudFormationModel
 from moto.moto_api._internal import mock_random as random
 from moto.route53.exceptions import (
+    ConflictingDomainExists,
     DnsNameInvalidForZone,
     HostedZoneNotEmpty,
     InvalidActionValue,
@@ -532,6 +533,24 @@ class Route53Backend(BaseBackend):
         self.query_logging_configs: Dict[str, QueryLoggingConfig] = {}
         self.delegation_sets: Dict[str, DelegationSet] = dict()
 
+    def _has_prev_conflicting_domain(
+        self, name: str, delegation_set_id: Optional[str]
+    ) -> bool:
+        """Check if a conflicting domain exists in the backend"""
+        if not delegation_set_id:
+            return False
+        for zone in self.zones.values():
+            if not zone.delegation_set or zone.delegation_set.id != delegation_set_id:
+                # Delegation sets don't match, so can't possibly conflict
+                continue
+            if (
+                zone.name == name
+                or zone.name.endswith(f".{name}")
+                or name.endswith(f".{zone.name}")
+            ):
+                return True
+        return False
+
     def create_hosted_zone(
         self,
         name: str,
@@ -542,6 +561,8 @@ class Route53Backend(BaseBackend):
         comment: Optional[str] = None,
         delegation_set_id: Optional[str] = None,
     ) -> FakeZone:
+        if self._has_prev_conflicting_domain(name, delegation_set_id):
+            raise ConflictingDomainExists(name, delegation_set_id)
         new_id = create_route53_zone_id()
         caller_reference = caller_reference or create_route53_caller_reference()
         delegation_set = self.create_reusable_delegation_set(
diff --git a/moto/s3/exceptions.py b/moto/s3/exceptions.py
index 337a9492e..dfea0dec6 100644
--- a/moto/s3/exceptions.py
+++ b/moto/s3/exceptions.py
@@ -2,6 +2,8 @@ from typing import TYPE_CHECKING, Any, Optional, Union
 
 from moto.core.exceptions import RESTError
 
+from .notifications import S3NotificationEvent
+
 if TYPE_CHECKING:
     from moto.s3.models import FakeDeleteMarker
 
@@ -281,10 +283,13 @@ class InvalidNotificationDestination(S3ClientError):
 class InvalidNotificationEvent(S3ClientError):
     code = 400
 
-    def __init__(self) -> None:
+    def __init__(self, event_name: str) -> None:
         super().__init__(
             "InvalidArgument",
-            "The event is not supported for notifications",
+            (
+                f"The event '{event_name}' is not supported for notifications. "
+                f"Supported events are as follows: {S3NotificationEvent.events()}"
+            ),
         )
 
 
diff --git a/moto/s3/models.py b/moto/s3/models.py
index c115f96c1..480601e47 100644
--- a/moto/s3/models.py
+++ b/moto/s3/models.py
@@ -2088,7 +2088,10 @@ class S3Backend(BaseBackend, CloudWatchMetricProvider):
         bucket.keys.setlist(key_name, keys)
 
         notifications.send_event(
-            self.account_id, notifications.S3_OBJECT_CREATE_PUT, bucket, new_key
+            self.account_id,
+            notifications.S3NotificationEvent.OBJECT_CREATED_PUT_EVENT,
+            bucket,
+            new_key,
         )
 
         return new_key
@@ -2640,7 +2643,10 @@ class S3Backend(BaseBackend, CloudWatchMetricProvider):
 
         # Send notifications that an object was copied
         notifications.send_event(
-            self.account_id, notifications.S3_OBJECT_CREATE_COPY, bucket, new_key
+            self.account_id,
+            notifications.S3NotificationEvent.OBJECT_CREATED_COPY_EVENT,
+            bucket,
+            new_key,
         )
 
     def put_bucket_acl(self, bucket_name: str, acl: Optional[FakeAcl]) -> None:
diff --git a/moto/s3/notifications.py b/moto/s3/notifications.py
index b43ab1908..389b60070 100644
--- a/moto/s3/notifications.py
+++ b/moto/s3/notifications.py
@@ -1,11 +1,53 @@
 import json
 from datetime import datetime
+from enum import Enum
 from typing import Any, Dict, List
 
 _EVENT_TIME_FORMAT = "%Y-%m-%dT%H:%M:%S.%f"
 
-S3_OBJECT_CREATE_COPY = "s3:ObjectCreated:Copy"
-S3_OBJECT_CREATE_PUT = "s3:ObjectCreated:Put"
+
+class S3NotificationEvent(str, Enum):
+    REDUCED_REDUNDANCY_LOST_OBJECT_EVENT = "s3:ReducedRedundancyLostObject"
+    OBJCET_CREATED_EVENT = "s3:ObjectCreated:*"
+    OBJECT_CREATED_PUT_EVENT = "s3:ObjectCreated:Put"
+    OBJECT_CREATED_POST_EVENT = "s3:ObjectCreated:Post"
+    OBJECT_CREATED_COPY_EVENT = "s3:ObjectCreated:Copy"
+    OBJECT_CREATED_COMPLETE_MULTIPART_UPLOAD_EVENT = (
+        "s3:ObjectCreated:CompleteMultipartUpload"
+    )
+    OBJECT_REMOVED_EVENT = "s3:ObjectRemoved:*"
+    OBJECTREMOVED_DELETE_EVENT = "s3:ObjectRemoved:Delete"
+    OBJECTREMOVED_DELETE_MARKER_CREATED_EVENT = "s3:ObjectRemoved:DeleteMarkerCreated"
+    OBJECT_RESTORE_EVENT = "s3:ObjectRestore:*"
+    OBJECT_RESTORE_POST_EVENT = "s3:ObjectRestore:Post"
+    OBJECT_RESTORE_COMPLETED_EVENT = "s3:ObjectRestore:Completed"
+    REPLICATION_EVENT = "s3:Replication:*"
+    REPLICATION_OPERATION_FAILED_REPLICATION_EVENT = (
+        "s3:Replication:OperationFailedReplication"
+    )
+    REPLICATION_OPERATION_NOT_TRACKED_EVENT = "s3:Replication:OperationNotTracked"
+    REPLICATION_OPERATION_MISSED_THRESHOLD_EVENT = (
+        "s3:Replication:OperationMissedThreshold"
+    )
+    REPLICATION_OPERATION_REPLICATED_AFTER_THRESHOLD_EVENT = (
+        "s3:Replication:OperationReplicatedAfterThreshold"
+    )
+    OBJECT_RESTORE_DELETE_EVENT = "s3:ObjectRestore:Delete"
+    LIFECYCLE_TRANSITION_EVENT = "s3:LifecycleTransition"
+    INTELLIGENT_TIERING_EVENT = "s3:IntelligentTiering"
+    OBJECT_ACL_EVENT = "s3:ObjectAcl:Put"
+    LIFECYCLE_EXPIRATION_EVENT = "s3:LifecycleExpiration:*"
+    LIFECYCLEEXPIRATION_DELETE_EVENT = "s3:LifecycleExpiration:Delete"
+    LIFECYCLE_EXPIRATION_DELETE_MARKER_CREATED_EVENT = (
+        "s3:LifecycleExpiration:DeleteMarkerCreated"
+    )
+    OBJECT_TAGGING_EVENT = "s3:ObjectTagging:*"
+    OBJECT_TAGGING_PUT_EVENT = "s3:ObjectTagging:Put"
+    OBJECTTAGGING_DELETE_EVENT = "s3:ObjectTagging:Delete"
+
+    @classmethod
+    def events(self) -> List[str]:
+        return sorted([item.value for item in S3NotificationEvent])
 
 
 def _get_s3_event(
@@ -41,7 +83,9 @@ def _get_region_from_arn(arn: str) -> str:
     return arn.split(":")[3]
 
 
-def send_event(account_id: str, event_name: str, bucket: Any, key: Any) -> None:
+def send_event(
+    account_id: str, event_name: S3NotificationEvent, bucket: Any, key: Any
+) -> None:
     if bucket.notification_configuration is None:
         return
 
diff --git a/moto/s3/responses.py b/moto/s3/responses.py
index efde8dea1..8d666ebf1 100644
--- a/moto/s3/responses.py
+++ b/moto/s3/responses.py
@@ -63,6 +63,7 @@ from .models import (
     get_canned_acl,
     s3_backends,
 )
+from .notifications import S3NotificationEvent
 from .select_object_content import serialize_select
 from .utils import (
     ARCHIVE_STORAGE_CLASSES,
@@ -2113,18 +2114,6 @@ class S3Response(BaseResponse):
             ("CloudFunction", "lambda"),
         ]
 
-        event_names = [
-            "s3:ReducedRedundancyLostObject",
-            "s3:ObjectCreated:*",
-            "s3:ObjectCreated:Put",
-            "s3:ObjectCreated:Post",
-            "s3:ObjectCreated:Copy",
-            "s3:ObjectCreated:CompleteMultipartUpload",
-            "s3:ObjectRemoved:*",
-            "s3:ObjectRemoved:Delete",
-            "s3:ObjectRemoved:DeleteMarkerCreated",
-        ]
-
         found_notifications = (
             0  # Tripwire -- if this is not ever set, then there were no notifications
         )
@@ -2151,8 +2140,8 @@ class S3Response(BaseResponse):
                         n["Event"] = [n["Event"]]
 
                     for event in n["Event"]:
-                        if event not in event_names:
-                            raise InvalidNotificationEvent()
+                        if event not in S3NotificationEvent.events():
+                            raise InvalidNotificationEvent(event)
 
                     # Parse out the filters:
                     if n.get("Filter"):
diff --git a/moto/sagemaker/exceptions.py b/moto/sagemaker/exceptions.py
index 8f5750573..c2286b9fe 100644
--- a/moto/sagemaker/exceptions.py
+++ b/moto/sagemaker/exceptions.py
@@ -41,6 +41,10 @@ class AWSValidationException(AWSError):
     TYPE = "ValidationException"
 
 
+class ResourceInUseException(AWSError):
+    TYPE = "ResourceInUse"
+
+
 class ResourceNotFound(JsonRESTError):
     def __init__(self, message: str):
         super().__init__(__class__.__name__, message)  # type: ignore
diff --git a/moto/sagemaker/models.py b/moto/sagemaker/models.py
index c284c65ec..a828ee3a2 100644
--- a/moto/sagemaker/models.py
+++ b/moto/sagemaker/models.py
@@ -14,6 +14,7 @@ from moto.utilities.paginator import paginate
 from .exceptions import (
     AWSValidationException,
     MissingModel,
+    ResourceInUseException,
     ResourceNotFound,
     ValidationError,
 )
@@ -1006,6 +1007,9 @@ class FeatureGroup(BaseObject):
             "Catalog": "AwsDataCatalog",
             "Database": "sagemaker_featurestore",
         }
+        offline_store_config["S3StorageConfig"][
+            "ResolvedOutputS3Uri"
+        ] = f'{offline_store_config["S3StorageConfig"]["S3Uri"]}/{account_id}/{region_name}/offline-store/{feature_group_name}-{int(datetime.now().timestamp())}/data'
 
         self.offline_store_config = offline_store_config
         self.role_arn = role_arn
@@ -3330,8 +3334,14 @@ class SageMakerModelBackend(BaseBackend):
     ) -> ModelPackageGroup:
         model_package_group = self.model_package_groups.get(model_package_group_name)
         if model_package_group is None:
+            model_package_group_arn = arn_formatter(
+                region_name=self.region_name,
+                account_id=self.account_id,
+                _type="model-package-group",
+                _id=f"{model_package_group_name}",
+            )
             raise ValidationError(
-                f"Model package group {model_package_group_name} not found"
+                f"ModelPackageGroup {model_package_group_arn} does not exist."
             )
         return model_package_group
 
@@ -3529,6 +3539,17 @@ class SageMakerModelBackend(BaseBackend):
         role_arn: str,
         tags: Any,
     ) -> str:
+        feature_group_arn = arn_formatter(
+            region_name=self.region_name,
+            account_id=self.account_id,
+            _type="feature-group",
+            _id=f"{feature_group_name.lower()}",
+        )
+        if feature_group_arn in self.feature_groups:
+            raise ResourceInUseException(
+                message=f"An error occurred (ResourceInUse) when calling the CreateFeatureGroup operation: Resource Already Exists: FeatureGroup with name {feature_group_name} already exists. Choose a different name.\nInfo: Feature Group '{feature_group_name}' already exists."
+            )
+
         feature_group = FeatureGroup(
             feature_group_name=feature_group_name,
             record_identifier_feature_name=record_identifier_feature_name,
diff --git a/moto/sns/models.py b/moto/sns/models.py
index e4a038977..c7f678a75 100644
--- a/moto/sns/models.py
+++ b/moto/sns/models.py
@@ -732,8 +732,17 @@ class SNSBackend(BaseBackend):
             raise SNSNotFoundError(
                 "Subscription does not exist", template="wrapped_single_error"
             )
+        # AWS does not return the FilterPolicy scope if the FilterPolicy is not set
+        # if the FilterPolicy is set and not the FilterPolicyScope, it returns the default value
+        attributes = {**subscription.attributes}
+        if "FilterPolicyScope" in attributes and not attributes.get("FilterPolicy"):
+            attributes.pop("FilterPolicyScope", None)
+            attributes.pop("FilterPolicy", None)
 
-        return subscription.attributes
+        elif "FilterPolicy" in attributes and "FilterPolicyScope" not in attributes:
+            attributes["FilterPolicyScope"] = "MessageAttributes"
+
+        return attributes
 
     def set_subscription_attributes(self, arn: str, name: str, value: Any) -> None:
         if name not in [
@@ -753,15 +762,19 @@ class SNSBackend(BaseBackend):
         subscription = _subscription[0]
 
         if name == "FilterPolicy":
-            filter_policy = json.loads(value)
-            # we validate the filter policy differently depending on the scope
-            # we need to always set the scope first
-            filter_policy_scope = subscription.attributes.get("FilterPolicyScope")
-            self._validate_filter_policy(filter_policy, scope=filter_policy_scope)
-            subscription._filter_policy = filter_policy
-            subscription._filter_policy_matcher = FilterPolicyMatcher(
-                filter_policy, filter_policy_scope
-            )
+            if value:
+                filter_policy = json.loads(value)
+                # we validate the filter policy differently depending on the scope
+                # we need to always set the scope first
+                filter_policy_scope = subscription.attributes.get("FilterPolicyScope")
+                self._validate_filter_policy(filter_policy, scope=filter_policy_scope)
+                subscription._filter_policy = filter_policy
+                subscription._filter_policy_matcher = FilterPolicyMatcher(
+                    filter_policy, filter_policy_scope
+                )
+            else:
+                subscription._filter_policy = None
+                subscription._filter_policy_matcher = None
 
         subscription.attributes[name] = value
 
diff --git a/setup.cfg b/setup.cfg
index 546553103..db489cfa8 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -105,7 +105,8 @@ appsync = graphql-core
 athena =
 autoscaling =
 awslambda = docker>=3.0.0
-awslambda_simple = 
+awslambda_simple =
+backup =
 batch = docker>=3.0.0
 batch_simple =
 budgets =
@@ -141,20 +142,20 @@ datapipeline =
 datasync =
 dax =
 dms =
-ds = sshpubkeys>=3.1.0
+ds =
 dynamodb =
     docker>=3.0.0
     py-partiql-parser==0.5.0
 dynamodbstreams =
     docker>=3.0.0
     py-partiql-parser==0.5.0
-ebs = sshpubkeys>=3.1.0
+ebs =
 ec2 = sshpubkeys>=3.1.0
 ec2instanceconnect =
 ecr =
 ecs =
-efs = sshpubkeys>=3.1.0
-eks = sshpubkeys>=3.1.0
+efs =
+eks =
 elasticache =
 elasticbeanstalk =
 elastictranscoder =
@@ -172,8 +173,10 @@ glue = pyparsing>=3.0.7
 greengrass =
 guardduty =
 iam =
+inspector2 =
 iot =
 iotdata = jsondiff>=1.1.2
+ivs =
 kinesis =
 kinesisvideo =
 kinesisvideoarchivedmedia =
@@ -189,6 +192,7 @@ meteringmarketplace =
 mq =
 opsworks =
 organizations =
+panorama =
 personalize =
 pinpoint =
 polly =
@@ -206,13 +210,12 @@ resourcegroupstaggingapi =
     graphql-core
     PyYAML>=5.1
     cfn-lint>=0.40.0
-    sshpubkeys>=3.1.0
     openapi-spec-validator>=0.5.0
     pyparsing>=3.0.7
     jsondiff>=1.1.2
     py-partiql-parser==0.5.0
 route53 =
-route53resolver = sshpubkeys>=3.1.0
+route53resolver =
 s3 =
     PyYAML>=5.1
     py-partiql-parser==0.5.0
@@ -223,6 +226,7 @@ s3crc32c =
 s3control =
 sagemaker =
 sdb =
+scheduler =
 secretsmanager =
 servicediscovery =
 servicequotas =


Test patch:
diff --git a/tests/test_backup/__init__.py b/tests/test_backup/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/tests/test_backup/test_backup.py b/tests/test_backup/test_backup.py
new file mode 100644
index 000000000..030bd9eb9
--- /dev/null
+++ b/tests/test_backup/test_backup.py
@@ -0,0 +1,359 @@
+"""Unit tests for backup-supported APIs."""
+import boto3
+import pytest
+from botocore.exceptions import ClientError
+
+from moto import mock_backup
+
+
+@mock_backup
+def test_create_backup_plan():
+    client = boto3.client("backup", region_name="eu-west-1")
+    response = client.create_backup_vault(
+        BackupVaultName="backupvault-foobar",
+    )
+    resp = client.create_backup_plan(
+        BackupPlan={
+            "BackupPlanName": "backupplan-foobar",
+            "Rules": [
+                {
+                    "RuleName": "foobar",
+                    "TargetBackupVaultName": response["BackupVaultName"],
+                },
+            ],
+        },
+    )
+    assert "BackupPlanId" in resp
+    assert "BackupPlanArn" in resp
+    assert "CreationDate" in resp
+    assert "VersionId" in resp
+
+
+@mock_backup
+def test_create_backup_plan_already_exists():
+    client = boto3.client("backup", region_name="eu-west-1")
+    backup_plan_name = "backup_plan_foobar"
+    rules = [
+        {
+            "RuleName": "foobar",
+            "TargetBackupVaultName": "backup-vault-foobar",
+        },
+    ]
+    client.create_backup_plan(
+        BackupPlan={"BackupPlanName": backup_plan_name, "Rules": rules}
+    )
+
+    with pytest.raises(ClientError) as exc:
+        client.create_backup_plan(
+            BackupPlan={"BackupPlanName": backup_plan_name, "Rules": rules}
+        )
+    err = exc.value.response["Error"]
+    assert err["Code"] == "AlreadyExistsException"
+
+
+@mock_backup
+def test_get_backup_plan():
+    client = boto3.client("backup", region_name="eu-west-1")
+    response = client.create_backup_vault(
+        BackupVaultName="backupvault-foobar",
+    )
+    plan = client.create_backup_plan(
+        BackupPlan={
+            "BackupPlanName": "backupplan-foobar",
+            "Rules": [
+                {
+                    "RuleName": "foobar",
+                    "TargetBackupVaultName": response["BackupVaultName"],
+                },
+            ],
+        },
+    )
+    resp = client.get_backup_plan(
+        BackupPlanId=plan["BackupPlanId"], VersionId=plan["VersionId"]
+    )
+    assert "BackupPlan" in resp
+
+
+@mock_backup
+def test_get_backup_plan_invalid_id():
+    client = boto3.client("backup", region_name="eu-west-1")
+
+    with pytest.raises(ClientError) as exc:
+        client.get_backup_plan(BackupPlanId="foobar")
+    err = exc.value.response["Error"]
+    assert err["Code"] == "ResourceNotFoundException"
+
+
+@mock_backup
+def test_get_backup_plan_invalid_version_id():
+    client = boto3.client("backup", region_name="eu-west-1")
+
+    plan = client.create_backup_plan(
+        BackupPlan={
+            "BackupPlanName": "backupplan-foobar",
+            "Rules": [
+                {
+                    "RuleName": "foobar",
+                    "TargetBackupVaultName": "Backup-vault-foobar",
+                },
+            ],
+        },
+    )
+    with pytest.raises(ClientError) as exc:
+        client.get_backup_plan(BackupPlanId=plan["BackupPlanId"], VersionId="foobar")
+    err = exc.value.response["Error"]
+    assert err["Code"] == "ResourceNotFoundException"
+
+
+@mock_backup
+def test_get_backup_plan_with_multiple_rules():
+    client = boto3.client("backup", region_name="eu-west-1")
+    plan = client.create_backup_plan(
+        BackupPlan={
+            "BackupPlanName": "backupplan-foobar",
+            "Rules": [
+                {
+                    "RuleName": "rule1",
+                    "TargetBackupVaultName": "backupvault-foobar",
+                    "ScheduleExpression": "cron(0 1 ? * * *)",
+                    "StartWindowMinutes": 60,
+                    "CompletionWindowMinutes": 120,
+                },
+                {
+                    "RuleName": "rule2",
+                    "TargetBackupVaultName": "backupvault-foobar",
+                },
+            ],
+        },
+    )
+    resp = client.get_backup_plan(BackupPlanId=plan["BackupPlanId"])
+    for rule in resp["BackupPlan"]["Rules"]:
+        assert "ScheduleExpression" in rule
+        assert "StartWindowMinutes" in rule
+        assert "CompletionWindowMinutes" in rule
+        assert "RuleId" in rule
+
+
+@mock_backup
+def test_delete_backup_plan():
+    client = boto3.client("backup", region_name="eu-west-1")
+    response = client.create_backup_vault(
+        BackupVaultName="backupvault-foobar",
+    )
+    plan = client.create_backup_plan(
+        BackupPlan={
+            "BackupPlanName": "backupplan-foobar",
+            "Rules": [
+                {
+                    "RuleName": "foobar",
+                    "TargetBackupVaultName": response["BackupVaultName"],
+                },
+            ],
+        },
+    )
+
+    resp = client.delete_backup_plan(BackupPlanId=plan["BackupPlanId"])
+    assert "BackupPlanId" in resp
+    assert "BackupPlanArn" in resp
+    assert "DeletionDate" in resp
+    assert "VersionId" in resp
+
+    resp = client.get_backup_plan(
+        BackupPlanId=plan["BackupPlanId"], VersionId=plan["VersionId"]
+    )
+    assert "DeletionDate" in resp
+
+
+@mock_backup
+def test_delete_backup_plan_invalid_id():
+    client = boto3.client("backup", region_name="eu-west-1")
+
+    with pytest.raises(ClientError) as exc:
+        client.delete_backup_plan(BackupPlanId="foobar")
+    err = exc.value.response["Error"]
+    assert err["Code"] == "ResourceNotFoundException"
+
+
+@mock_backup
+def test_list_backup_plans():
+    client = boto3.client("backup", region_name="eu-west-1")
+    for i in range(1, 3):
+        client.create_backup_plan(
+            BackupPlan={
+                "BackupPlanName": f"backup-plan-{i}",
+                "Rules": [
+                    {
+                        "RuleName": "foobar",
+                        "TargetBackupVaultName": "backupvault-foobar",
+                    },
+                ],
+            },
+        )
+    resp = client.list_backup_plans()
+    backup_plans = resp["BackupPlansList"]
+    assert backup_plans[0]["BackupPlanName"] == "backup-plan-1"
+    assert backup_plans[1]["BackupPlanName"] == "backup-plan-2"
+    assert len(backup_plans) == 2
+    assert resp["ResponseMetadata"]["HTTPStatusCode"] == 200
+
+
+@mock_backup
+def test_list_backup_plans_without_include_deleted():
+    client = boto3.client("backup", region_name="eu-west-1")
+
+    for i in range(1, 3):
+        client.create_backup_plan(
+            BackupPlan={
+                "BackupPlanName": f"backup-plan-{i}",
+                "Rules": [
+                    {
+                        "RuleName": "foobar",
+                        "TargetBackupVaultName": "backupvault-foobar",
+                    },
+                ],
+            },
+        )
+    resp = client.list_backup_plans()
+    client.delete_backup_plan(BackupPlanId=resp["BackupPlansList"][0]["BackupPlanId"])
+    resp_list = client.list_backup_plans()
+    backup_plans = resp_list["BackupPlansList"]
+    assert backup_plans[0]["BackupPlanName"] == "backup-plan-2"
+    assert len(backup_plans) == 1
+
+
+@mock_backup
+def test_list_backup_plans_with_include_deleted():
+    client = boto3.client("backup", region_name="eu-west-1")
+    for i in range(1, 3):
+        client.create_backup_plan(
+            BackupPlan={
+                "BackupPlanName": f"backup-plan-{i}",
+                "Rules": [
+                    {
+                        "RuleName": "foobar",
+                        "TargetBackupVaultName": "backupvault-foobar",
+                    },
+                ],
+            },
+        )
+    resp = client.list_backup_plans()
+    client.delete_backup_plan(BackupPlanId=resp["BackupPlansList"][0]["BackupPlanId"])
+    resp_list = client.list_backup_plans(IncludeDeleted=True)
+    backup_plans = resp_list["BackupPlansList"]
+    assert backup_plans[0]["BackupPlanName"] == "backup-plan-1"
+    assert backup_plans[1]["BackupPlanName"] == "backup-plan-2"
+    assert len(backup_plans) == 2
+
+
+@mock_backup
+def test_create_backup_vault():
+    client = boto3.client("backup", region_name="eu-west-1")
+    resp = client.create_backup_vault(
+        BackupVaultName="backupvault-foobar",
+        BackupVaultTags={
+            "foo": "bar",
+        },
+    )
+    assert "BackupVaultName" in resp
+    assert "BackupVaultArn" in resp
+    assert "CreationDate" in resp
+
+
+@mock_backup
+def test_create_backup_vault_already_exists():
+    client = boto3.client("backup", region_name="eu-west-1")
+    backup_vault_name = "backup_vault_foobar"
+    client.create_backup_vault(BackupVaultName=backup_vault_name)
+
+    with pytest.raises(ClientError) as exc:
+        client.create_backup_vault(BackupVaultName=backup_vault_name)
+    err = exc.value.response["Error"]
+    assert err["Code"] == "AlreadyExistsException"
+
+
+@mock_backup
+def test_list_backup_vaults():
+    client = boto3.client("backup", region_name="eu-west-1")
+    for i in range(1, 3):
+        client.create_backup_vault(
+            BackupVaultName=f"backup-vault-{i}",
+        )
+    resp = client.list_backup_vaults()
+    backup_plans = resp["BackupVaultList"]
+    assert backup_plans[0]["BackupVaultName"] == "backup-vault-1"
+    assert backup_plans[1]["BackupVaultName"] == "backup-vault-2"
+    assert resp["ResponseMetadata"]["HTTPStatusCode"] == 200
+
+
+@mock_backup
+def test_list_tags_vault():
+    client = boto3.client("backup", region_name="eu-west-1")
+    vault = client.create_backup_vault(
+        BackupVaultName="backupvault-foobar",
+        BackupVaultTags={
+            "key1": "value1",
+            "key2": "value2",
+        },
+    )
+    resp = client.list_tags(ResourceArn=vault["BackupVaultArn"])
+    assert resp["Tags"] == {"key1": "value1", "key2": "value2"}
+
+
+@mock_backup
+def test_list_tags_plan():
+    client = boto3.client("backup", region_name="eu-west-1")
+    response = client.create_backup_vault(
+        BackupVaultName="backupvault-foobar",
+    )
+    plan = client.create_backup_plan(
+        BackupPlan={
+            "BackupPlanName": "backupplan-foobar",
+            "Rules": [
+                {
+                    "RuleName": "foobar",
+                    "TargetBackupVaultName": response["BackupVaultName"],
+                },
+            ],
+        },
+        BackupPlanTags={
+            "key1": "value1",
+            "key2": "value2",
+        },
+    )
+    resp = client.list_tags(ResourceArn=plan["BackupPlanArn"])
+    assert resp["Tags"] == {"key1": "value1", "key2": "value2"}
+
+
+@mock_backup
+def test_tag_resource():
+    client = boto3.client("backup", region_name="eu-west-1")
+    vault = client.create_backup_vault(
+        BackupVaultName="backupvault-foobar",
+        BackupVaultTags={
+            "key1": "value1",
+        },
+    )
+    resource_arn = vault["BackupVaultArn"]
+    client.tag_resource(
+        ResourceArn=resource_arn, Tags={"key2": "value2", "key3": "value3"}
+    )
+    resp = client.list_tags(ResourceArn=resource_arn)
+    assert resp["Tags"] == {"key1": "value1", "key2": "value2", "key3": "value3"}
+
+
+@mock_backup
+def test_untag_resource():
+    client = boto3.client("backup", region_name="eu-west-1")
+    vault = client.create_backup_vault(
+        BackupVaultName="backupvault-foobar",
+        BackupVaultTags={
+            "key1": "value1",
+        },
+    )
+    resource_arn = vault["BackupVaultArn"]
+    client.tag_resource(
+        ResourceArn=resource_arn, Tags={"key2": "value2", "key3": "value3"}
+    )
+    resp = client.untag_resource(ResourceArn=resource_arn, TagKeyList=["key2"])
+    resp = client.list_tags(ResourceArn=resource_arn)
+    assert resp["Tags"] == {"key1": "value1", "key3": "value3"}
diff --git a/tests/test_core/test_utils.py b/tests/test_core/test_utils.py
index cc2e86749..7a8915b91 100644
--- a/tests/test_core/test_utils.py
+++ b/tests/test_core/test_utils.py
@@ -1,3 +1,5 @@
+import datetime
+
 import pytest
 from freezegun import freeze_time
 
@@ -5,6 +7,8 @@ from moto.core.utils import (
     camelcase_to_pascal,
     camelcase_to_underscores,
     pascal_to_camelcase,
+    rfc_1123_datetime,
+    str_to_rfc_1123_datetime,
     underscores_to_camelcase,
     unix_time,
 )
@@ -50,3 +54,10 @@ def test_camelcase_to_pascal(_input: str, expected: str) -> None:
 @freeze_time("2015-01-01 12:00:00")
 def test_unix_time() -> None:  # type: ignore[misc]
     assert unix_time() == 1420113600.0
+
+
+def test_rfc1123_dates() -> None:
+    with freeze_time("2012-03-04 05:00:05"):
+        x = rfc_1123_datetime(datetime.datetime.now())
+        assert x == "Sun, 04 Mar 2012 05:00:05 GMT"
+        assert str_to_rfc_1123_datetime(x) == datetime.datetime.now()
diff --git a/tests/test_resourcegroupstaggingapi/test_resourcegroupstaggingapi.py b/tests/test_resourcegroupstaggingapi/test_resourcegroupstaggingapi.py
index 88fc908c1..079466116 100644
--- a/tests/test_resourcegroupstaggingapi/test_resourcegroupstaggingapi.py
+++ b/tests/test_resourcegroupstaggingapi/test_resourcegroupstaggingapi.py
@@ -5,6 +5,7 @@ from botocore.client import ClientError
 
 from moto import (
     mock_acm,
+    mock_backup,
     mock_cloudformation,
     mock_ec2,
     mock_ecs,
@@ -117,6 +118,37 @@ def test_get_resources_acm():
     )
 
 
+@mock_backup
+@mock_resourcegroupstaggingapi
+def test_get_resources_backup():
+    backup = boto3.client("backup", region_name="eu-central-1")
+
+    # Create two tagged Backup Vaults
+    for i in range(1, 3):
+        i_str = str(i)
+
+        backup.create_backup_vault(
+            BackupVaultName="backup-vault-tag-" + i_str,
+            BackupVaultTags={
+                "Test": i_str,
+            },
+        )
+
+    rtapi = boto3.client("resourcegroupstaggingapi", region_name="eu-central-1")
+
+    # Basic test
+    resp = rtapi.get_resources(ResourceTypeFilters=["backup"])
+    assert len(resp["ResourceTagMappingList"]) == 2
+
+    # Test tag filtering
+    resp = rtapi.get_resources(
+        ResourceTypeFilters=["backup"],
+        TagFilters=[{"Key": "Test", "Values": ["1"]}],
+    )
+    assert len(resp["ResourceTagMappingList"]) == 1
+    assert {"Key": "Test", "Value": "1"} in resp["ResourceTagMappingList"][0]["Tags"]
+
+
 @mock_ecs
 @mock_ec2
 @mock_resourcegroupstaggingapi
diff --git a/tests/test_route53/test_route53.py b/tests/test_route53/test_route53.py
index 149e34958..a40418523 100644
--- a/tests/test_route53/test_route53.py
+++ b/tests/test_route53/test_route53.py
@@ -1516,3 +1516,54 @@ def test_get_dns_sec():
     )["HostedZone"]["Id"]
     dns_sec = client.get_dnssec(HostedZoneId=hosted_zone_id)
     assert dns_sec["Status"] == {"ServeSignature": "NOT_SIGNING"}
+
+
+@mock_route53
+@pytest.mark.parametrize(
+    "domain1,domain2",
+    (
+        ["a.com", "a.com"],
+        ["a.b.com", "b.com"],
+        ["b.com", "a.b.com"],
+        ["a.b.com", "a.b.com"],
+    ),
+)
+def test_conflicting_domain_exists(domain1, domain2):
+    delegation_set_id = "N10015061S366L6NMTRKQ"
+    conn = boto3.client("route53", region_name="us-east-1")
+    conn.create_hosted_zone(
+        Name=domain1,
+        CallerReference=str(hash("foo")),
+        DelegationSetId=delegation_set_id,
+    )
+    with pytest.raises(ClientError) as exc_info:
+        conn.create_hosted_zone(
+            Name=domain2,
+            CallerReference=str(hash("bar")),
+            DelegationSetId=delegation_set_id,
+        )
+    assert exc_info.value.response.get("Error").get("Code") == "ConflictingDomainExists"
+    for string in [delegation_set_id, domain2]:
+        assert string in exc_info.value.response.get("Error").get("Message")
+
+    # Now test that these domains can be created with different delegation set ids
+    conn.create_hosted_zone(
+        Name=domain1,
+        CallerReference=str(hash("foo")),
+    )
+    conn.create_hosted_zone(
+        Name=domain2,
+        CallerReference=str(hash("bar")),
+    )
+
+    # And, finally, test that these domains can be created with different named delegation sets
+    conn.create_hosted_zone(
+        Name=domain1,
+        CallerReference=str(hash("foo")),
+        DelegationSetId="1",
+    )
+    conn.create_hosted_zone(
+        Name=domain2,
+        CallerReference=str(hash("bar")),
+        DelegationSetId="2",
+    )
diff --git a/tests/test_s3/test_s3.py b/tests/test_s3/test_s3.py
index f9deba6bc..6b6312355 100644
--- a/tests/test_s3/test_s3.py
+++ b/tests/test_s3/test_s3.py
@@ -2336,7 +2336,7 @@ def test_put_bucket_notification_errors():
     assert err.value.response["Error"]["Code"] == "InvalidArgument"
     assert (
         err.value.response["Error"]["Message"]
-        == "The event is not supported for notifications"
+        == "The event 'notarealeventname' is not supported for notifications. Supported events are as follows: ['s3:IntelligentTiering', 's3:LifecycleExpiration:*', 's3:LifecycleExpiration:Delete', 's3:LifecycleExpiration:DeleteMarkerCreated', 's3:LifecycleTransition', 's3:ObjectAcl:Put', 's3:ObjectCreated:*', 's3:ObjectCreated:CompleteMultipartUpload', 's3:ObjectCreated:Copy', 's3:ObjectCreated:Post', 's3:ObjectCreated:Put', 's3:ObjectRemoved:*', 's3:ObjectRemoved:Delete', 's3:ObjectRemoved:DeleteMarkerCreated', 's3:ObjectRestore:*', 's3:ObjectRestore:Completed', 's3:ObjectRestore:Delete', 's3:ObjectRestore:Post', 's3:ObjectTagging:*', 's3:ObjectTagging:Delete', 's3:ObjectTagging:Put', 's3:ReducedRedundancyLostObject', 's3:Replication:*', 's3:Replication:OperationFailedReplication', 's3:Replication:OperationMissedThreshold', 's3:Replication:OperationNotTracked', 's3:Replication:OperationReplicatedAfterThreshold']"
     )
 
 
diff --git a/tests/test_s3/test_s3_locales.py b/tests/test_s3/test_s3_locales.py
new file mode 100644
index 000000000..06aca5c36
--- /dev/null
+++ b/tests/test_s3/test_s3_locales.py
@@ -0,0 +1,30 @@
+from datetime import datetime
+from unittest import SkipTest
+
+import boto3
+from freezegun import freeze_time
+
+from moto import mock_s3, settings
+
+
+@mock_s3
+def test_rfc_returns_valid_date_for_every_possible_weekday_and_month():
+    if not settings.TEST_DECORATOR_MODE:
+        raise SkipTest("Freezing time only possible in DecoratorMode")
+    client = boto3.client("s3", region_name="us-east-1")
+    client.create_bucket(Bucket="bucket_")
+    for weekday in range(1, 8):
+        with freeze_time(f"2023-02-{weekday} 12:00:00"):
+            client.put_object(Bucket="bucket_", Key="test.txt", Body=b"test")
+            obj = client.get_object(Bucket="bucket_", Key="test.txt")
+            assert obj["LastModified"].replace(tzinfo=None) == datetime.now()
+            # If we get here, the LastModified date will have been successfully parsed
+            # Regardless of which weekday it is
+
+    for month in range(1, 13):
+        with freeze_time(f"2023-{month}-02 12:00:00"):
+            client.put_object(Bucket="bucket_", Key="test.txt", Body=b"test")
+            obj = client.get_object(Bucket="bucket_", Key="test.txt")
+            assert obj["LastModified"].replace(tzinfo=None) == datetime.now()
+            # If we get here, the LastModified date will have been successfully parsed
+            # Regardless of which month it is
diff --git a/tests/test_sagemaker/test_sagemaker_feature_groups.py b/tests/test_sagemaker/test_sagemaker_feature_groups.py
index c3f4a1475..029b042c3 100644
--- a/tests/test_sagemaker/test_sagemaker_feature_groups.py
+++ b/tests/test_sagemaker/test_sagemaker_feature_groups.py
@@ -3,6 +3,8 @@ import re
 from datetime import datetime
 
 import boto3
+import pytest
+from botocore.exceptions import ClientError
 
 from moto import mock_sagemaker
 
@@ -34,6 +36,29 @@ def test_create_feature_group():
         == "arn:aws:sagemaker:us-east-2:123456789012:feature-group/some-feature-group-name"
     )
 
+    with pytest.raises(ClientError) as raised_exception:
+        client.create_feature_group(
+            FeatureGroupName="some-feature-group-name",
+            RecordIdentifierFeatureName="some_record_identifier",
+            EventTimeFeatureName="EventTime",
+            FeatureDefinitions=[
+                {"FeatureName": "some_feature", "FeatureType": "String"},
+                {"FeatureName": "EventTime", "FeatureType": "Fractional"},
+                {"FeatureName": "some_record_identifier", "FeatureType": "String"},
+            ],
+            RoleArn="arn:aws:iam::123456789012:role/AWSFeatureStoreAccess",
+            OfflineStoreConfig={
+                "DisableGlueTableCreation": False,
+                "S3StorageConfig": {"S3Uri": "s3://mybucket"},
+            },
+        )
+
+    assert raised_exception.value.response["Error"]["Code"] == "ResourceInUse"
+    assert (
+        raised_exception.value.response["Error"]["Message"]
+        == "An error occurred (ResourceInUse) when calling the CreateFeatureGroup operation: Resource Already Exists: FeatureGroup with name some-feature-group-name already exists. Choose a different name.\nInfo: Feature Group 'some-feature-group-name' already exists."
+    )
+
 
 @mock_sagemaker
 def test_describe_feature_group():
@@ -55,7 +80,7 @@ def test_describe_feature_group():
         RoleArn=role_arn,
         OfflineStoreConfig={
             "DisableGlueTableCreation": False,
-            "S3StorageConfig": {"S3Uri": "s3://mybucket"},
+            "S3StorageConfig": {"S3Uri": "s3://mybucket/some-folder/some-subfolder"},
         },
     )
     resp = client.describe_feature_group(FeatureGroupName=feature_group_name)
@@ -70,7 +95,7 @@ def test_describe_feature_group():
     assert resp["FeatureDefinitions"] == feature_definitions
     assert resp["RoleArn"] == role_arn
     assert re.match(
-        r"^some_feature_group_name_[0-9]+$",
+        f"^{feature_group_name.replace('-', '_')}_[0-9]+$",
         resp["OfflineStoreConfig"]["DataCatalogConfig"]["TableName"],
     )
     assert (
@@ -80,6 +105,13 @@ def test_describe_feature_group():
         resp["OfflineStoreConfig"]["DataCatalogConfig"]["Database"]
         == "sagemaker_featurestore"
     )
-    assert resp["OfflineStoreConfig"]["S3StorageConfig"]["S3Uri"] == "s3://mybucket"
+    assert (
+        resp["OfflineStoreConfig"]["S3StorageConfig"]["S3Uri"]
+        == "s3://mybucket/some-folder/some-subfolder"
+    )
+    assert re.match(
+        f"^s3://mybucket/some-folder/some-subfolder/123456789012/us-east-2/offline-store/{feature_group_name}-[0-9]+/data$",
+        resp["OfflineStoreConfig"]["S3StorageConfig"]["ResolvedOutputS3Uri"],
+    )
     assert isinstance(resp["CreationTime"], datetime)
     assert resp["FeatureGroupStatus"] == "Created"
diff --git a/tests/test_sagemaker/test_sagemaker_model_package_groups.py b/tests/test_sagemaker/test_sagemaker_model_package_groups.py
index 15ba43a8a..d9d50a261 100644
--- a/tests/test_sagemaker/test_sagemaker_model_package_groups.py
+++ b/tests/test_sagemaker/test_sagemaker_model_package_groups.py
@@ -4,6 +4,8 @@ from datetime import datetime
 from unittest import SkipTest
 
 import boto3
+import pytest
+from botocore.exceptions import ClientError
 from dateutil.tz import tzutc  # type: ignore
 from freezegun import freeze_time
 
@@ -193,6 +195,21 @@ def test_describe_model_package_group():
     assert resp["CreationTime"] == datetime(2020, 1, 1, 0, 0, 0, tzinfo=tzutc())
 
 
+@mock_sagemaker
+def test_describe_model_package_group_not_exists():
+    if settings.TEST_SERVER_MODE:
+        raise SkipTest("Can't freeze time in ServerMode")
+    client = boto3.client("sagemaker", region_name="eu-west-1")
+
+    with pytest.raises(ClientError) as e:
+        client.describe_model_package_group(
+            ModelPackageGroupName="test-model-package-group"
+        )
+
+    assert e.value.response["Error"]["Code"] == "ValidationException"
+    assert "does not exist" in e.value.response["Error"]["Message"]
+
+
 @mock_sagemaker
 def test_list_tags_model_package_group():
     region_name = "eu-west-1"
diff --git a/tests/test_sns/test_subscriptions_boto3.py b/tests/test_sns/test_subscriptions_boto3.py
index 700ccd32a..a63df5f50 100644
--- a/tests/test_sns/test_subscriptions_boto3.py
+++ b/tests/test_sns/test_subscriptions_boto3.py
@@ -393,6 +393,7 @@ def test_set_subscription_attributes():
     assert attrs["Attributes"]["RawMessageDelivery"] == "true"
     assert attrs["Attributes"]["DeliveryPolicy"] == delivery_policy
     assert attrs["Attributes"]["FilterPolicy"] == filter_policy
+    assert attrs["Attributes"]["FilterPolicyScope"] == "MessageAttributes"
 
     filter_policy_scope = "MessageBody"
     conn.set_subscription_attributes(
@@ -405,6 +406,17 @@ def test_set_subscription_attributes():
 
     assert attrs["Attributes"]["FilterPolicyScope"] == filter_policy_scope
 
+    # test unsetting a filter policy
+    conn.set_subscription_attributes(
+        SubscriptionArn=subscription_arn,
+        AttributeName="FilterPolicy",
+        AttributeValue="",
+    )
+
+    attrs = conn.get_subscription_attributes(SubscriptionArn=subscription_arn)
+    assert "FilterPolicy" not in attrs["Attributes"]
+    assert "FilterPolicyScope" not in attrs["Attributes"]
+
     # not existing subscription
     with pytest.raises(ClientError):
         conn.set_subscription_attributes(
@@ -413,7 +425,7 @@ def test_set_subscription_attributes():
             AttributeValue="true",
         )
     with pytest.raises(ClientError):
-        attrs = conn.get_subscription_attributes(SubscriptionArn="invalid")
+        conn.get_subscription_attributes(SubscriptionArn="invalid")
 
     # invalid attr name
     with pytest.raises(ClientError):