Repository: cloud-custodian/cloud-custodian

Issue:
Support for AWS Timestream
### Describe the feature

Support for AWS timestream that includes adding support for tagging and deletion of resource

### Extra information or context

_No response_


Discussion:

Resolving patch:
diff --git a/c7n/resources/resource_map.py b/c7n/resources/resource_map.py
--- a/c7n/resources/resource_map.py
+++ b/c7n/resources/resource_map.py
@@ -213,6 +213,8 @@
     "aws.subnet": "c7n.resources.vpc.Subnet",
     "aws.support-case": "c7n.resources.support.SupportCase",
     "aws.swf-domain": "c7n.resources.swf.SimpleWorkflowDomain",
+    "aws.timestream-database": "c7n.resources.timestream.TimestreamDatabase",
+    "aws.timestream-table": "c7n.resources.timestream.TimestreamTable",
     "aws.transfer-server": "c7n.resources.transfer.TransferServer",
     "aws.transfer-user": "c7n.resources.transfer.TransferUser",
     "aws.transit-attachment": "c7n.resources.vpc.TransitGatewayAttachment",
diff --git a/c7n/resources/timestream.py b/c7n/resources/timestream.py
new file mode 100644
--- /dev/null
+++ b/c7n/resources/timestream.py
@@ -0,0 +1,137 @@
+from c7n.manager import resources
+from c7n.actions import Action
+from c7n.query import DescribeSource, QueryResourceManager, TypeInfo
+from c7n.utils import local_session, type_schema
+from c7n.tags import (
+    TagDelayedAction,
+    TagActionFilter,
+    Tag as TagAction,
+    RemoveTag as RemoveTagAction
+)
+
+
+class DescribeTimestream(DescribeSource):
+    def augment(self, resources):
+        for r in resources:
+            client = local_session(self.manager.session_factory).client('timestream-write')
+            r['Tags'] = client.list_tags_for_resource(ResourceARN=r['Arn'])['Tags']
+        return resources
+
+
+@resources.register('timestream-database')
+class TimestreamDatabase(QueryResourceManager):
+    class resource_type(TypeInfo):
+        service = 'timestream-write'
+        arn_type = ''
+        name = 'DatabaseName'
+        id = arn = 'Arn'
+        enum_spec = ('list_databases', 'Databases', {})
+        permission_prefix = 'timestream'
+        permissions = ('timestream:ListDatabases', )
+
+    source_mapping = {
+        'describe': DescribeTimestream,
+    }
+
+
+@resources.register('timestream-table')
+class TimestreamTable(QueryResourceManager):
+    class resource_type(TypeInfo):
+        service = 'timestream-write'
+        arn_type = ''
+        name = 'TableName'
+        id = arn = 'Arn'
+        enum_spec = ('list_tables', 'Tables', {})
+        permission_prefix = 'timestream'
+        permissions = ('timestream:ListTables', )
+
+    source_mapping = {
+        'describe': DescribeTimestream,
+    }
+
+
+@TimestreamDatabase.action_registry.register('tag')
+@TimestreamTable.action_registry.register('tag')
+class TimestreamTag(TagAction):
+
+    permissions = ('timestream:TagResource', )
+
+    def process_resource_set(self, client, resource_set, tags):
+        for r in resource_set:
+            client.tag_resource(ResourceARN=r['Arn'], Tags=tags)
+
+
+@TimestreamDatabase.action_registry.register('remove-tag')
+@TimestreamTable.action_registry.register('remove-tag')
+class TimestreamRemoveTag(RemoveTagAction):
+
+    permissions = ('timestream:UntagResource', )
+
+    def process_resource_set(self, client, resource_set, tag_keys):
+        for r in resource_set:
+            client.untag_resource(ResourceARN=r['Arn'], TagKeys=tag_keys)
+
+
+TimestreamDatabase.action_registry.register('mark-for-op', TagDelayedAction)
+TimestreamTable.action_registry.register('mark-for-op', TagDelayedAction)
+
+TimestreamDatabase.filter_registry.register('marked-for-op', TagActionFilter)
+TimestreamTable.filter_registry.register('marked-for-op', TagActionFilter)
+
+
+@TimestreamTable.action_registry.register('delete')
+class TimestreamTableDelete(Action):
+    """
+    Deletes a timestream table
+    """
+
+    schema = type_schema('delete')
+    permissions = ('timestream:DeleteTable', )
+
+    def process(self, resources):
+        client = local_session(self.manager.session_factory).client('timestream-write')
+        for r in resources:
+            try:
+                client.delete_table(
+                    DatabaseName=r['DatabaseName'],
+                    TableName=r['TableName']
+                )
+            except client.exceptions.ResourceNotFoundException:
+                continue
+
+
+@TimestreamDatabase.action_registry.register('delete')
+class TimestreamDatabaseDelete(Action):
+    """
+    Deletes a timestream database
+    """
+
+    schema = type_schema('delete', force={'type': 'boolean', 'default': False})
+    permissions = (
+        'timestream:DeleteDatabase',
+        'timestream:ListTables', 'timestream:DeleteTable', )
+
+    def process(self, resources):
+        client = local_session(self.manager.session_factory).client('timestream-write')
+        for r in resources:
+            try:
+                client.delete_database(
+                    DatabaseName=r['DatabaseName'],
+                )
+            except client.exceptions.ResourceNotFoundException:
+                continue
+            except client.exceptions.ValidationException:
+                if not self.data.get('force', False):
+                    self.log.error(
+                        f'Unable to delete database:{r["DatabaseName"]}, '
+                        'tables must be deleted first')
+                    continue
+                tables = client.list_tables(DatabaseName=r['DatabaseName'])['Tables']
+                TimestreamTableDelete(
+                    data={'type': 'delete'},
+                    manager=self.manager,
+                    log_dir=self.log_dir
+                ).process(tables)
+                client.delete_database(
+                    DatabaseName=r['DatabaseName'],
+                )


Test patch:
diff --git a/tests/data/placebo/test_timestream_database_delete/ingest.timestream.DeleteDatabase_1.json b/tests/data/placebo/test_timestream_database_delete/ingest.timestream.DeleteDatabase_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_timestream_database_delete/ingest.timestream.DeleteDatabase_1.json
@@ -0,0 +1,6 @@
+{
+    "status_code": 200,
+    "data": {
+        "ResponseMetadata": {}
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_timestream_database_delete/ingest.timestream.DescribeEndpoints_1.json b/tests/data/placebo/test_timestream_database_delete/ingest.timestream.DescribeEndpoints_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_timestream_database_delete/ingest.timestream.DescribeEndpoints_1.json
@@ -0,0 +1,12 @@
+{
+    "status_code": 200,
+    "data": {
+        "Endpoints": [
+            {
+                "Address": "ingest-cell2.timestream.us-east-1.amazonaws.com",
+                "CachePeriodInMinutes": 1440
+            }
+        ],
+        "ResponseMetadata": {}
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_timestream_database_delete/ingest.timestream.DescribeEndpoints_2.json b/tests/data/placebo/test_timestream_database_delete/ingest.timestream.DescribeEndpoints_2.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_timestream_database_delete/ingest.timestream.DescribeEndpoints_2.json
@@ -0,0 +1,12 @@
+{
+    "status_code": 200,
+    "data": {
+        "Endpoints": [
+            {
+                "Address": "ingest-cell2.timestream.us-east-1.amazonaws.com",
+                "CachePeriodInMinutes": 1440
+            }
+        ],
+        "ResponseMetadata": {}
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_timestream_database_delete/ingest.timestream.DescribeEndpoints_3.json b/tests/data/placebo/test_timestream_database_delete/ingest.timestream.DescribeEndpoints_3.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_timestream_database_delete/ingest.timestream.DescribeEndpoints_3.json
@@ -0,0 +1,12 @@
+{
+    "status_code": 200,
+    "data": {
+        "Endpoints": [
+            {
+                "Address": "ingest-cell2.timestream.us-east-1.amazonaws.com",
+                "CachePeriodInMinutes": 1440
+            }
+        ],
+        "ResponseMetadata": {}
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_timestream_database_delete/ingest.timestream.DescribeEndpoints_4.json b/tests/data/placebo/test_timestream_database_delete/ingest.timestream.DescribeEndpoints_4.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_timestream_database_delete/ingest.timestream.DescribeEndpoints_4.json
@@ -0,0 +1,12 @@
+{
+    "status_code": 200,
+    "data": {
+        "Endpoints": [
+            {
+                "Address": "ingest-cell2.timestream.us-east-1.amazonaws.com",
+                "CachePeriodInMinutes": 1440
+            }
+        ],
+        "ResponseMetadata": {}
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_timestream_database_delete/ingest.timestream.ListDatabases_1.json b/tests/data/placebo/test_timestream_database_delete/ingest.timestream.ListDatabases_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_timestream_database_delete/ingest.timestream.ListDatabases_1.json
@@ -0,0 +1,34 @@
+{
+    "status_code": 200,
+    "data": {
+        "Databases": [
+            {
+                "Arn": "arn:aws:timestream:us-east-1:644160558196:database/ljlkjlkj",
+                "DatabaseName": "ljlkjlkj",
+                "TableCount": 0,
+                "KmsKeyId": "arn:aws:kms:us-east-1:644160558196:key/eb703ede-7c05-4cfc-b650-5c6f31b8e019",
+                "CreationTime": {
+                    "__class__": "datetime",
+                    "year": 2023,
+                    "month": 1,
+                    "day": 13,
+                    "hour": 15,
+                    "minute": 11,
+                    "second": 46,
+                    "microsecond": 947000
+                },
+                "LastUpdatedTime": {
+                    "__class__": "datetime",
+                    "year": 2023,
+                    "month": 1,
+                    "day": 13,
+                    "hour": 15,
+                    "minute": 11,
+                    "second": 46,
+                    "microsecond": 941000
+                }
+            }
+        ],
+        "ResponseMetadata": {}
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_timestream_database_delete/ingest.timestream.ListDatabases_2.json b/tests/data/placebo/test_timestream_database_delete/ingest.timestream.ListDatabases_2.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_timestream_database_delete/ingest.timestream.ListDatabases_2.json
@@ -0,0 +1,7 @@
+{
+    "status_code": 200,
+    "data": {
+        "Databases": [],
+        "ResponseMetadata": {}
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_timestream_database_delete/ingest.timestream.ListTagsForResource_1.json b/tests/data/placebo/test_timestream_database_delete/ingest.timestream.ListTagsForResource_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_timestream_database_delete/ingest.timestream.ListTagsForResource_1.json
@@ -0,0 +1,12 @@
+{
+    "status_code": 200,
+    "data": {
+        "Tags": [
+            {
+                "Key": "maid_status",
+                "Value": "Resource does not meet policy: tag@2023/01/13"
+            }
+        ],
+        "ResponseMetadata": {}
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_timestream_database_delete_force/ingest.timestream.DeleteDatabase_1.json b/tests/data/placebo/test_timestream_database_delete_force/ingest.timestream.DeleteDatabase_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_timestream_database_delete_force/ingest.timestream.DeleteDatabase_1.json
@@ -0,0 +1,11 @@
+{
+    "status_code": 400,
+    "data": {
+        "Error": {
+            "Message": "The database contains 1 table(s). You must delete these tables before deleting the database.",
+            "Code": "ValidationException"
+        },
+        "ResponseMetadata": {},
+        "Message": "The database contains 1 table(s). You must delete these tables before deleting the database."
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_timestream_database_delete_force/ingest.timestream.DeleteDatabase_2.json b/tests/data/placebo/test_timestream_database_delete_force/ingest.timestream.DeleteDatabase_2.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_timestream_database_delete_force/ingest.timestream.DeleteDatabase_2.json
@@ -0,0 +1,6 @@
+{
+    "status_code": 200,
+    "data": {
+        "ResponseMetadata": {}
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_timestream_database_delete_force/ingest.timestream.DeleteTable_1.json b/tests/data/placebo/test_timestream_database_delete_force/ingest.timestream.DeleteTable_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_timestream_database_delete_force/ingest.timestream.DeleteTable_1.json
@@ -0,0 +1,6 @@
+{
+    "status_code": 200,
+    "data": {
+        "ResponseMetadata": {}
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_timestream_database_delete_force/ingest.timestream.DescribeEndpoints_1.json b/tests/data/placebo/test_timestream_database_delete_force/ingest.timestream.DescribeEndpoints_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_timestream_database_delete_force/ingest.timestream.DescribeEndpoints_1.json
@@ -0,0 +1,12 @@
+{
+    "status_code": 200,
+    "data": {
+        "Endpoints": [
+            {
+                "Address": "ingest-cell2.timestream.us-east-1.amazonaws.com",
+                "CachePeriodInMinutes": 1440
+            }
+        ],
+        "ResponseMetadata": {}
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_timestream_database_delete_force/ingest.timestream.DescribeEndpoints_2.json b/tests/data/placebo/test_timestream_database_delete_force/ingest.timestream.DescribeEndpoints_2.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_timestream_database_delete_force/ingest.timestream.DescribeEndpoints_2.json
@@ -0,0 +1,12 @@
+{
+    "status_code": 200,
+    "data": {
+        "Endpoints": [
+            {
+                "Address": "ingest-cell2.timestream.us-east-1.amazonaws.com",
+                "CachePeriodInMinutes": 1440
+            }
+        ],
+        "ResponseMetadata": {}
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_timestream_database_delete_force/ingest.timestream.DescribeEndpoints_3.json b/tests/data/placebo/test_timestream_database_delete_force/ingest.timestream.DescribeEndpoints_3.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_timestream_database_delete_force/ingest.timestream.DescribeEndpoints_3.json
@@ -0,0 +1,12 @@
+{
+    "status_code": 200,
+    "data": {
+        "Endpoints": [
+            {
+                "Address": "ingest-cell2.timestream.us-east-1.amazonaws.com",
+                "CachePeriodInMinutes": 1440
+            }
+        ],
+        "ResponseMetadata": {}
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_timestream_database_delete_force/ingest.timestream.DescribeEndpoints_4.json b/tests/data/placebo/test_timestream_database_delete_force/ingest.timestream.DescribeEndpoints_4.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_timestream_database_delete_force/ingest.timestream.DescribeEndpoints_4.json
@@ -0,0 +1,12 @@
+{
+    "status_code": 200,
+    "data": {
+        "Endpoints": [
+            {
+                "Address": "ingest-cell2.timestream.us-east-1.amazonaws.com",
+                "CachePeriodInMinutes": 1440
+            }
+        ],
+        "ResponseMetadata": {}
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_timestream_database_delete_force/ingest.timestream.DescribeEndpoints_5.json b/tests/data/placebo/test_timestream_database_delete_force/ingest.timestream.DescribeEndpoints_5.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_timestream_database_delete_force/ingest.timestream.DescribeEndpoints_5.json
@@ -0,0 +1,12 @@
+{
+    "status_code": 200,
+    "data": {
+        "Endpoints": [
+            {
+                "Address": "ingest-cell2.timestream.us-east-1.amazonaws.com",
+                "CachePeriodInMinutes": 1440
+            }
+        ],
+        "ResponseMetadata": {}
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_timestream_database_delete_force/ingest.timestream.ListDatabases_1.json b/tests/data/placebo/test_timestream_database_delete_force/ingest.timestream.ListDatabases_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_timestream_database_delete_force/ingest.timestream.ListDatabases_1.json
@@ -0,0 +1,34 @@
+{
+    "status_code": 200,
+    "data": {
+        "Databases": [
+            {
+                "Arn": "arn:aws:timestream:us-east-1:644160558196:database/asdfasdfasdf",
+                "DatabaseName": "asdfasdfasdf",
+                "TableCount": 1,
+                "KmsKeyId": "arn:aws:kms:us-east-1:644160558196:key/eb703ede-7c05-4cfc-b650-5c6f31b8e019",
+                "CreationTime": {
+                    "__class__": "datetime",
+                    "year": 2023,
+                    "month": 1,
+                    "day": 13,
+                    "hour": 16,
+                    "minute": 10,
+                    "second": 35,
+                    "microsecond": 480000
+                },
+                "LastUpdatedTime": {
+                    "__class__": "datetime",
+                    "year": 2023,
+                    "month": 1,
+                    "day": 13,
+                    "hour": 16,
+                    "minute": 10,
+                    "second": 35,
+                    "microsecond": 473000
+                }
+            }
+        ],
+        "ResponseMetadata": {}
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_timestream_database_delete_force/ingest.timestream.ListDatabases_2.json b/tests/data/placebo/test_timestream_database_delete_force/ingest.timestream.ListDatabases_2.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_timestream_database_delete_force/ingest.timestream.ListDatabases_2.json
@@ -0,0 +1,7 @@
+{
+    "status_code": 200,
+    "data": {
+        "Databases": [],
+        "ResponseMetadata": {}
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_timestream_database_delete_force/ingest.timestream.ListTables_1.json b/tests/data/placebo/test_timestream_database_delete_force/ingest.timestream.ListTables_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_timestream_database_delete_force/ingest.timestream.ListTables_1.json
@@ -0,0 +1,41 @@
+{
+    "status_code": 200,
+    "data": {
+        "Tables": [
+            {
+                "Arn": "arn:aws:timestream:us-east-1:644160558196:database/asdfasdfasdf/table/IoTMulti",
+                "TableName": "IoTMulti",
+                "DatabaseName": "asdfasdfasdf",
+                "TableStatus": "ACTIVE",
+                "RetentionProperties": {
+                    "MemoryStoreRetentionPeriodInHours": 24,
+                    "MagneticStoreRetentionPeriodInDays": 1825
+                },
+                "CreationTime": {
+                    "__class__": "datetime",
+                    "year": 2023,
+                    "month": 1,
+                    "day": 13,
+                    "hour": 16,
+                    "minute": 10,
+                    "second": 36,
+                    "microsecond": 181000
+                },
+                "LastUpdatedTime": {
+                    "__class__": "datetime",
+                    "year": 2023,
+                    "month": 1,
+                    "day": 13,
+                    "hour": 16,
+                    "minute": 10,
+                    "second": 36,
+                    "microsecond": 181000
+                },
+                "MagneticStoreWriteProperties": {
+                    "EnableMagneticStoreWrites": false
+                }
+            }
+        ],
+        "ResponseMetadata": {}
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_timestream_database_delete_force/ingest.timestream.ListTagsForResource_1.json b/tests/data/placebo/test_timestream_database_delete_force/ingest.timestream.ListTagsForResource_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_timestream_database_delete_force/ingest.timestream.ListTagsForResource_1.json
@@ -0,0 +1,7 @@
+{
+    "status_code": 200,
+    "data": {
+        "Tags": [],
+        "ResponseMetadata": {}
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_timestream_database_remove_tag/ingest.timestream.DescribeEndpoints_1.json b/tests/data/placebo/test_timestream_database_remove_tag/ingest.timestream.DescribeEndpoints_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_timestream_database_remove_tag/ingest.timestream.DescribeEndpoints_1.json
@@ -0,0 +1,12 @@
+{
+    "status_code": 200,
+    "data": {
+        "Endpoints": [
+            {
+                "Address": "ingest-cell2.timestream.us-east-1.amazonaws.com",
+                "CachePeriodInMinutes": 1440
+            }
+        ],
+        "ResponseMetadata": {}
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_timestream_database_remove_tag/ingest.timestream.DescribeEndpoints_2.json b/tests/data/placebo/test_timestream_database_remove_tag/ingest.timestream.DescribeEndpoints_2.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_timestream_database_remove_tag/ingest.timestream.DescribeEndpoints_2.json
@@ -0,0 +1,12 @@
+{
+    "status_code": 200,
+    "data": {
+        "Endpoints": [
+            {
+                "Address": "ingest-cell2.timestream.us-east-1.amazonaws.com",
+                "CachePeriodInMinutes": 1440
+            }
+        ],
+        "ResponseMetadata": {}
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_timestream_database_remove_tag/ingest.timestream.DescribeEndpoints_3.json b/tests/data/placebo/test_timestream_database_remove_tag/ingest.timestream.DescribeEndpoints_3.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_timestream_database_remove_tag/ingest.timestream.DescribeEndpoints_3.json
@@ -0,0 +1,12 @@
+{
+    "status_code": 200,
+    "data": {
+        "Endpoints": [
+            {
+                "Address": "ingest-cell2.timestream.us-east-1.amazonaws.com",
+                "CachePeriodInMinutes": 1440
+            }
+        ],
+        "ResponseMetadata": {}
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_timestream_database_remove_tag/ingest.timestream.DescribeEndpoints_4.json b/tests/data/placebo/test_timestream_database_remove_tag/ingest.timestream.DescribeEndpoints_4.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_timestream_database_remove_tag/ingest.timestream.DescribeEndpoints_4.json
@@ -0,0 +1,12 @@
+{
+    "status_code": 200,
+    "data": {
+        "Endpoints": [
+            {
+                "Address": "ingest-cell2.timestream.us-east-1.amazonaws.com",
+                "CachePeriodInMinutes": 1440
+            }
+        ],
+        "ResponseMetadata": {}
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_timestream_database_remove_tag/ingest.timestream.ListDatabases_1.json b/tests/data/placebo/test_timestream_database_remove_tag/ingest.timestream.ListDatabases_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_timestream_database_remove_tag/ingest.timestream.ListDatabases_1.json
@@ -0,0 +1,34 @@
+{
+    "status_code": 200,
+    "data": {
+        "Databases": [
+            {
+                "Arn": "arn:aws:timestream:us-east-1:644160558196:database/ljlkjlkj",
+                "DatabaseName": "ljlkjlkj",
+                "TableCount": 1,
+                "KmsKeyId": "arn:aws:kms:us-east-1:644160558196:key/eb703ede-7c05-4cfc-b650-5c6f31b8e019",
+                "CreationTime": {
+                    "__class__": "datetime",
+                    "year": 2023,
+                    "month": 1,
+                    "day": 13,
+                    "hour": 15,
+                    "minute": 11,
+                    "second": 46,
+                    "microsecond": 947000
+                },
+                "LastUpdatedTime": {
+                    "__class__": "datetime",
+                    "year": 2023,
+                    "month": 1,
+                    "day": 13,
+                    "hour": 15,
+                    "minute": 11,
+                    "second": 46,
+                    "microsecond": 941000
+                }
+            }
+        ],
+        "ResponseMetadata": {}
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_timestream_database_remove_tag/ingest.timestream.ListTagsForResource_1.json b/tests/data/placebo/test_timestream_database_remove_tag/ingest.timestream.ListTagsForResource_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_timestream_database_remove_tag/ingest.timestream.ListTagsForResource_1.json
@@ -0,0 +1,12 @@
+{
+    "status_code": 200,
+    "data": {
+        "Tags": [
+            {
+                "Key": "foo",
+                "Value": "bar"
+            }
+        ],
+        "ResponseMetadata": {}
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_timestream_database_remove_tag/ingest.timestream.ListTagsForResource_2.json b/tests/data/placebo/test_timestream_database_remove_tag/ingest.timestream.ListTagsForResource_2.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_timestream_database_remove_tag/ingest.timestream.ListTagsForResource_2.json
@@ -0,0 +1,7 @@
+{
+    "status_code": 200,
+    "data": {
+        "Tags": [],
+        "ResponseMetadata": {}
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_timestream_database_remove_tag/ingest.timestream.UntagResource_1.json b/tests/data/placebo/test_timestream_database_remove_tag/ingest.timestream.UntagResource_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_timestream_database_remove_tag/ingest.timestream.UntagResource_1.json
@@ -0,0 +1,6 @@
+{
+    "status_code": 200,
+    "data": {
+        "ResponseMetadata": {}
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_timestream_database_tag/ingest.timestream.DescribeEndpoints_1.json b/tests/data/placebo/test_timestream_database_tag/ingest.timestream.DescribeEndpoints_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_timestream_database_tag/ingest.timestream.DescribeEndpoints_1.json
@@ -0,0 +1,12 @@
+{
+    "status_code": 200,
+    "data": {
+        "Endpoints": [
+            {
+                "Address": "ingest-cell2.timestream.us-east-1.amazonaws.com",
+                "CachePeriodInMinutes": 1440
+            }
+        ],
+        "ResponseMetadata": {}
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_timestream_database_tag/ingest.timestream.DescribeEndpoints_2.json b/tests/data/placebo/test_timestream_database_tag/ingest.timestream.DescribeEndpoints_2.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_timestream_database_tag/ingest.timestream.DescribeEndpoints_2.json
@@ -0,0 +1,12 @@
+{
+    "status_code": 200,
+    "data": {
+        "Endpoints": [
+            {
+                "Address": "ingest-cell2.timestream.us-east-1.amazonaws.com",
+                "CachePeriodInMinutes": 1440
+            }
+        ],
+        "ResponseMetadata": {}
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_timestream_database_tag/ingest.timestream.DescribeEndpoints_3.json b/tests/data/placebo/test_timestream_database_tag/ingest.timestream.DescribeEndpoints_3.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_timestream_database_tag/ingest.timestream.DescribeEndpoints_3.json
@@ -0,0 +1,12 @@
+{
+    "status_code": 200,
+    "data": {
+        "Endpoints": [
+            {
+                "Address": "ingest-cell2.timestream.us-east-1.amazonaws.com",
+                "CachePeriodInMinutes": 1440
+            }
+        ],
+        "ResponseMetadata": {}
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_timestream_database_tag/ingest.timestream.DescribeEndpoints_4.json b/tests/data/placebo/test_timestream_database_tag/ingest.timestream.DescribeEndpoints_4.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_timestream_database_tag/ingest.timestream.DescribeEndpoints_4.json
@@ -0,0 +1,12 @@
+{
+    "status_code": 200,
+    "data": {
+        "Endpoints": [
+            {
+                "Address": "ingest-cell2.timestream.us-east-1.amazonaws.com",
+                "CachePeriodInMinutes": 1440
+            }
+        ],
+        "ResponseMetadata": {}
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_timestream_database_tag/ingest.timestream.ListDatabases_1.json b/tests/data/placebo/test_timestream_database_tag/ingest.timestream.ListDatabases_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_timestream_database_tag/ingest.timestream.ListDatabases_1.json
@@ -0,0 +1,34 @@
+{
+    "status_code": 200,
+    "data": {
+        "Databases": [
+            {
+                "Arn": "arn:aws:timestream:us-east-1:644160558196:database/ljlkjlkj",
+                "DatabaseName": "ljlkjlkj",
+                "TableCount": 1,
+                "KmsKeyId": "arn:aws:kms:us-east-1:644160558196:key/eb703ede-7c05-4cfc-b650-5c6f31b8e019",
+                "CreationTime": {
+                    "__class__": "datetime",
+                    "year": 2023,
+                    "month": 1,
+                    "day": 13,
+                    "hour": 15,
+                    "minute": 11,
+                    "second": 46,
+                    "microsecond": 947000
+                },
+                "LastUpdatedTime": {
+                    "__class__": "datetime",
+                    "year": 2023,
+                    "month": 1,
+                    "day": 13,
+                    "hour": 15,
+                    "minute": 11,
+                    "second": 46,
+                    "microsecond": 941000
+                }
+            }
+        ],
+        "ResponseMetadata": {}
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_timestream_database_tag/ingest.timestream.ListTagsForResource_1.json b/tests/data/placebo/test_timestream_database_tag/ingest.timestream.ListTagsForResource_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_timestream_database_tag/ingest.timestream.ListTagsForResource_1.json
@@ -0,0 +1,7 @@
+{
+    "status_code": 200,
+    "data": {
+        "Tags": [],
+        "ResponseMetadata": {}
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_timestream_database_tag/ingest.timestream.ListTagsForResource_2.json b/tests/data/placebo/test_timestream_database_tag/ingest.timestream.ListTagsForResource_2.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_timestream_database_tag/ingest.timestream.ListTagsForResource_2.json
@@ -0,0 +1,12 @@
+{
+    "status_code": 200,
+    "data": {
+        "Tags": [
+            {
+                "Key": "foo",
+                "Value": "bar"
+            }
+        ],
+        "ResponseMetadata": {}
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_timestream_database_tag/ingest.timestream.TagResource_1.json b/tests/data/placebo/test_timestream_database_tag/ingest.timestream.TagResource_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_timestream_database_tag/ingest.timestream.TagResource_1.json
@@ -0,0 +1,6 @@
+{
+    "status_code": 200,
+    "data": {
+        "ResponseMetadata": {}
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_timestream_table_delete/ingest.timestream.DeleteTable_1.json b/tests/data/placebo/test_timestream_table_delete/ingest.timestream.DeleteTable_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_timestream_table_delete/ingest.timestream.DeleteTable_1.json
@@ -0,0 +1,6 @@
+{
+    "status_code": 200,
+    "data": {
+        "ResponseMetadata": {}
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_timestream_table_delete/ingest.timestream.DescribeEndpoints_1.json b/tests/data/placebo/test_timestream_table_delete/ingest.timestream.DescribeEndpoints_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_timestream_table_delete/ingest.timestream.DescribeEndpoints_1.json
@@ -0,0 +1,12 @@
+{
+    "status_code": 200,
+    "data": {
+        "Endpoints": [
+            {
+                "Address": "ingest-cell2.timestream.us-east-1.amazonaws.com",
+                "CachePeriodInMinutes": 1440
+            }
+        ],
+        "ResponseMetadata": {}
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_timestream_table_delete/ingest.timestream.DescribeEndpoints_2.json b/tests/data/placebo/test_timestream_table_delete/ingest.timestream.DescribeEndpoints_2.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_timestream_table_delete/ingest.timestream.DescribeEndpoints_2.json
@@ -0,0 +1,12 @@
+{
+    "status_code": 200,
+    "data": {
+        "Endpoints": [
+            {
+                "Address": "ingest-cell2.timestream.us-east-1.amazonaws.com",
+                "CachePeriodInMinutes": 1440
+            }
+        ],
+        "ResponseMetadata": {}
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_timestream_table_delete/ingest.timestream.DescribeEndpoints_3.json b/tests/data/placebo/test_timestream_table_delete/ingest.timestream.DescribeEndpoints_3.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_timestream_table_delete/ingest.timestream.DescribeEndpoints_3.json
@@ -0,0 +1,12 @@
+{
+    "status_code": 200,
+    "data": {
+        "Endpoints": [
+            {
+                "Address": "ingest-cell2.timestream.us-east-1.amazonaws.com",
+                "CachePeriodInMinutes": 1440
+            }
+        ],
+        "ResponseMetadata": {}
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_timestream_table_delete/ingest.timestream.DescribeEndpoints_4.json b/tests/data/placebo/test_timestream_table_delete/ingest.timestream.DescribeEndpoints_4.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_timestream_table_delete/ingest.timestream.DescribeEndpoints_4.json
@@ -0,0 +1,12 @@
+{
+    "status_code": 200,
+    "data": {
+        "Endpoints": [
+            {
+                "Address": "ingest-cell2.timestream.us-east-1.amazonaws.com",
+                "CachePeriodInMinutes": 1440
+            }
+        ],
+        "ResponseMetadata": {}
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_timestream_table_delete/ingest.timestream.ListTables_1.json b/tests/data/placebo/test_timestream_table_delete/ingest.timestream.ListTables_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_timestream_table_delete/ingest.timestream.ListTables_1.json
@@ -0,0 +1,41 @@
+{
+    "status_code": 200,
+    "data": {
+        "Tables": [
+            {
+                "Arn": "arn:aws:timestream:us-east-1:644160558196:database/ljlkjlkj/table/IoTMulti",
+                "TableName": "IoTMulti",
+                "DatabaseName": "ljlkjlkj",
+                "TableStatus": "ACTIVE",
+                "RetentionProperties": {
+                    "MemoryStoreRetentionPeriodInHours": 24,
+                    "MagneticStoreRetentionPeriodInDays": 1825
+                },
+                "CreationTime": {
+                    "__class__": "datetime",
+                    "year": 2023,
+                    "month": 1,
+                    "day": 13,
+                    "hour": 15,
+                    "minute": 11,
+                    "second": 47,
+                    "microsecond": 739000
+                },
+                "LastUpdatedTime": {
+                    "__class__": "datetime",
+                    "year": 2023,
+                    "month": 1,
+                    "day": 13,
+                    "hour": 15,
+                    "minute": 11,
+                    "second": 47,
+                    "microsecond": 733000
+                },
+                "MagneticStoreWriteProperties": {
+                    "EnableMagneticStoreWrites": false
+                }
+            }
+        ],
+        "ResponseMetadata": {}
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_timestream_table_delete/ingest.timestream.ListTables_2.json b/tests/data/placebo/test_timestream_table_delete/ingest.timestream.ListTables_2.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_timestream_table_delete/ingest.timestream.ListTables_2.json
@@ -0,0 +1,7 @@
+{
+    "status_code": 200,
+    "data": {
+        "Tables": [],
+        "ResponseMetadata": {}
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_timestream_table_delete/ingest.timestream.ListTagsForResource_1.json b/tests/data/placebo/test_timestream_table_delete/ingest.timestream.ListTagsForResource_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_timestream_table_delete/ingest.timestream.ListTagsForResource_1.json
@@ -0,0 +1,12 @@
+{
+    "status_code": 200,
+    "data": {
+        "Tags": [
+            {
+                "Key": "maid_status",
+                "Value": "Resource does not meet policy: tag@2023/01/13"
+            }
+        ],
+        "ResponseMetadata": {}
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_timestream_table_remove_tag/ingest.timestream.DescribeEndpoints_1.json b/tests/data/placebo/test_timestream_table_remove_tag/ingest.timestream.DescribeEndpoints_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_timestream_table_remove_tag/ingest.timestream.DescribeEndpoints_1.json
@@ -0,0 +1,12 @@
+{
+    "status_code": 200,
+    "data": {
+        "Endpoints": [
+            {
+                "Address": "ingest-cell2.timestream.us-east-1.amazonaws.com",
+                "CachePeriodInMinutes": 1440
+            }
+        ],
+        "ResponseMetadata": {}
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_timestream_table_remove_tag/ingest.timestream.DescribeEndpoints_2.json b/tests/data/placebo/test_timestream_table_remove_tag/ingest.timestream.DescribeEndpoints_2.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_timestream_table_remove_tag/ingest.timestream.DescribeEndpoints_2.json
@@ -0,0 +1,12 @@
+{
+    "status_code": 200,
+    "data": {
+        "Endpoints": [
+            {
+                "Address": "ingest-cell2.timestream.us-east-1.amazonaws.com",
+                "CachePeriodInMinutes": 1440
+            }
+        ],
+        "ResponseMetadata": {}
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_timestream_table_remove_tag/ingest.timestream.DescribeEndpoints_3.json b/tests/data/placebo/test_timestream_table_remove_tag/ingest.timestream.DescribeEndpoints_3.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_timestream_table_remove_tag/ingest.timestream.DescribeEndpoints_3.json
@@ -0,0 +1,12 @@
+{
+    "status_code": 200,
+    "data": {
+        "Endpoints": [
+            {
+                "Address": "ingest-cell2.timestream.us-east-1.amazonaws.com",
+                "CachePeriodInMinutes": 1440
+            }
+        ],
+        "ResponseMetadata": {}
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_timestream_table_remove_tag/ingest.timestream.DescribeEndpoints_4.json b/tests/data/placebo/test_timestream_table_remove_tag/ingest.timestream.DescribeEndpoints_4.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_timestream_table_remove_tag/ingest.timestream.DescribeEndpoints_4.json
@@ -0,0 +1,12 @@
+{
+    "status_code": 200,
+    "data": {
+        "Endpoints": [
+            {
+                "Address": "ingest-cell2.timestream.us-east-1.amazonaws.com",
+                "CachePeriodInMinutes": 1440
+            }
+        ],
+        "ResponseMetadata": {}
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_timestream_table_remove_tag/ingest.timestream.ListTables_1.json b/tests/data/placebo/test_timestream_table_remove_tag/ingest.timestream.ListTables_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_timestream_table_remove_tag/ingest.timestream.ListTables_1.json
@@ -0,0 +1,41 @@
+{
+    "status_code": 200,
+    "data": {
+        "Tables": [
+            {
+                "Arn": "arn:aws:timestream:us-east-1:644160558196:database/ljlkjlkj/table/IoTMulti",
+                "TableName": "IoTMulti",
+                "DatabaseName": "ljlkjlkj",
+                "TableStatus": "ACTIVE",
+                "RetentionProperties": {
+                    "MemoryStoreRetentionPeriodInHours": 24,
+                    "MagneticStoreRetentionPeriodInDays": 1825
+                },
+                "CreationTime": {
+                    "__class__": "datetime",
+                    "year": 2023,
+                    "month": 1,
+                    "day": 13,
+                    "hour": 15,
+                    "minute": 11,
+                    "second": 47,
+                    "microsecond": 739000
+                },
+                "LastUpdatedTime": {
+                    "__class__": "datetime",
+                    "year": 2023,
+                    "month": 1,
+                    "day": 13,
+                    "hour": 15,
+                    "minute": 11,
+                    "second": 47,
+                    "microsecond": 733000
+                },
+                "MagneticStoreWriteProperties": {
+                    "EnableMagneticStoreWrites": false
+                }
+            }
+        ],
+        "ResponseMetadata": {}
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_timestream_table_remove_tag/ingest.timestream.ListTagsForResource_1.json b/tests/data/placebo/test_timestream_table_remove_tag/ingest.timestream.ListTagsForResource_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_timestream_table_remove_tag/ingest.timestream.ListTagsForResource_1.json
@@ -0,0 +1,12 @@
+{
+    "status_code": 200,
+    "data": {
+        "Tags": [
+            {
+                "Key": "foo",
+                "Value": "bar"
+            }
+        ],
+        "ResponseMetadata": {}
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_timestream_table_remove_tag/ingest.timestream.ListTagsForResource_2.json b/tests/data/placebo/test_timestream_table_remove_tag/ingest.timestream.ListTagsForResource_2.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_timestream_table_remove_tag/ingest.timestream.ListTagsForResource_2.json
@@ -0,0 +1,7 @@
+{
+    "status_code": 200,
+    "data": {
+        "Tags": [],
+        "ResponseMetadata": {}
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_timestream_table_remove_tag/ingest.timestream.UntagResource_1.json b/tests/data/placebo/test_timestream_table_remove_tag/ingest.timestream.UntagResource_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_timestream_table_remove_tag/ingest.timestream.UntagResource_1.json
@@ -0,0 +1,6 @@
+{
+    "status_code": 200,
+    "data": {
+        "ResponseMetadata": {}
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_timestream_table_tag/ingest.timestream.DescribeEndpoints_1.json b/tests/data/placebo/test_timestream_table_tag/ingest.timestream.DescribeEndpoints_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_timestream_table_tag/ingest.timestream.DescribeEndpoints_1.json
@@ -0,0 +1,12 @@
+{
+    "status_code": 200,
+    "data": {
+        "Endpoints": [
+            {
+                "Address": "ingest-cell2.timestream.us-east-1.amazonaws.com",
+                "CachePeriodInMinutes": 1440
+            }
+        ],
+        "ResponseMetadata": {}
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_timestream_table_tag/ingest.timestream.DescribeEndpoints_2.json b/tests/data/placebo/test_timestream_table_tag/ingest.timestream.DescribeEndpoints_2.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_timestream_table_tag/ingest.timestream.DescribeEndpoints_2.json
@@ -0,0 +1,12 @@
+{
+    "status_code": 200,
+    "data": {
+        "Endpoints": [
+            {
+                "Address": "ingest-cell2.timestream.us-east-1.amazonaws.com",
+                "CachePeriodInMinutes": 1440
+            }
+        ],
+        "ResponseMetadata": {}
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_timestream_table_tag/ingest.timestream.DescribeEndpoints_3.json b/tests/data/placebo/test_timestream_table_tag/ingest.timestream.DescribeEndpoints_3.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_timestream_table_tag/ingest.timestream.DescribeEndpoints_3.json
@@ -0,0 +1,12 @@
+{
+    "status_code": 200,
+    "data": {
+        "Endpoints": [
+            {
+                "Address": "ingest-cell2.timestream.us-east-1.amazonaws.com",
+                "CachePeriodInMinutes": 1440
+            }
+        ],
+        "ResponseMetadata": {}
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_timestream_table_tag/ingest.timestream.DescribeEndpoints_4.json b/tests/data/placebo/test_timestream_table_tag/ingest.timestream.DescribeEndpoints_4.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_timestream_table_tag/ingest.timestream.DescribeEndpoints_4.json
@@ -0,0 +1,12 @@
+{
+    "status_code": 200,
+    "data": {
+        "Endpoints": [
+            {
+                "Address": "ingest-cell2.timestream.us-east-1.amazonaws.com",
+                "CachePeriodInMinutes": 1440
+            }
+        ],
+        "ResponseMetadata": {}
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_timestream_table_tag/ingest.timestream.ListTables_1.json b/tests/data/placebo/test_timestream_table_tag/ingest.timestream.ListTables_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_timestream_table_tag/ingest.timestream.ListTables_1.json
@@ -0,0 +1,41 @@
+{
+    "status_code": 200,
+    "data": {
+        "Tables": [
+            {
+                "Arn": "arn:aws:timestream:us-east-1:644160558196:database/ljlkjlkj/table/IoTMulti",
+                "TableName": "IoTMulti",
+                "DatabaseName": "ljlkjlkj",
+                "TableStatus": "ACTIVE",
+                "RetentionProperties": {
+                    "MemoryStoreRetentionPeriodInHours": 24,
+                    "MagneticStoreRetentionPeriodInDays": 1825
+                },
+                "CreationTime": {
+                    "__class__": "datetime",
+                    "year": 2023,
+                    "month": 1,
+                    "day": 13,
+                    "hour": 15,
+                    "minute": 11,
+                    "second": 47,
+                    "microsecond": 739000
+                },
+                "LastUpdatedTime": {
+                    "__class__": "datetime",
+                    "year": 2023,
+                    "month": 1,
+                    "day": 13,
+                    "hour": 15,
+                    "minute": 11,
+                    "second": 47,
+                    "microsecond": 733000
+                },
+                "MagneticStoreWriteProperties": {
+                    "EnableMagneticStoreWrites": false
+                }
+            }
+        ],
+        "ResponseMetadata": {}
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_timestream_table_tag/ingest.timestream.ListTagsForResource_1.json b/tests/data/placebo/test_timestream_table_tag/ingest.timestream.ListTagsForResource_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_timestream_table_tag/ingest.timestream.ListTagsForResource_1.json
@@ -0,0 +1,7 @@
+{
+    "status_code": 200,
+    "data": {
+        "Tags": [],
+        "ResponseMetadata": {}
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_timestream_table_tag/ingest.timestream.ListTagsForResource_2.json b/tests/data/placebo/test_timestream_table_tag/ingest.timestream.ListTagsForResource_2.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_timestream_table_tag/ingest.timestream.ListTagsForResource_2.json
@@ -0,0 +1,12 @@
+{
+    "status_code": 200,
+    "data": {
+        "Tags": [
+            {
+                "Key": "foo",
+                "Value": "bar"
+            }
+        ],
+        "ResponseMetadata": {}
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_timestream_table_tag/ingest.timestream.TagResource_1.json b/tests/data/placebo/test_timestream_table_tag/ingest.timestream.TagResource_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_timestream_table_tag/ingest.timestream.TagResource_1.json
@@ -0,0 +1,6 @@
+{
+    "status_code": 200,
+    "data": {
+        "ResponseMetadata": {}
+    }
+}
\ No newline at end of file
diff --git a/tests/test_timestream.py b/tests/test_timestream.py
new file mode 100644
--- /dev/null
+++ b/tests/test_timestream.py
@@ -0,0 +1,172 @@
+# Copyright The Cloud Custodian Authors.
+# SPDX-License-Identifier: Apache-2.0
+from .common import BaseTest
+
+
+class TestTimestreamDatabase(BaseTest):
+    def test_timestream_database_tag(self):
+        session_factory = self.replay_flight_data('test_timestream_database_tag')
+        p = self.load_policy(
+            {
+                'name': 'test-timestream-db-tag',
+                'resource': 'aws.timestream-database',
+                'filters': [
+                    {
+                        'tag:foo': 'absent'
+                    }
+                ],
+                'actions': [
+                    {
+                        'type': 'tag',
+                        'tags': {'foo': 'bar'}
+                    }
+                ]
+            }, session_factory=session_factory
+        )
+        resources = p.run()
+        self.assertEqual(len(resources), 1)
+        client = session_factory().client('timestream-write')
+        tags = client.list_tags_for_resource(ResourceARN=resources[0]['Arn'])['Tags']
+        self.assertEqual(len(tags), 1)
+        self.assertEqual(tags[0]['Key'], 'foo')
+        self.assertEqual(tags[0]['Value'], 'bar')
+
+    def test_timestream_database_remove_tag(self):
+        session_factory = self.replay_flight_data('test_timestream_database_remove_tag')
+        p = self.load_policy(
+            {
+                'name': 'test-timestream-db-tag',
+                'resource': 'aws.timestream-database',
+                'filters': [
+                    {
+                        'tag:foo': 'present'
+                    }
+                ],
+                'actions': [
+                    {
+                        'type': 'remove-tag',
+                        'tags': ['foo']
+                    }
+                ]
+            }, session_factory=session_factory
+        )
+        resources = p.run()
+        self.assertEqual(len(resources), 1)
+        client = session_factory().client('timestream-write')
+        tags = client.list_tags_for_resource(ResourceARN=resources[0]['Arn'])['Tags']
+        self.assertEqual(len(tags), 0)
+
+    def test_timestream_database_delete(self):
+        session_factory = self.replay_flight_data('test_timestream_database_delete')
+        p = self.load_policy(
+            {
+                'name': 'test-timestream-db-delete',
+                'resource': 'aws.timestream-database',
+                'actions': [
+                    {
+                        'type': 'delete',
+                    }
+                ]
+            }, session_factory=session_factory
+        )
+        resources = p.run()
+        self.assertEqual(len(resources), 1)
+        client = session_factory().client('timestream-write')
+        dbs = client.list_databases()['Databases']
+        self.assertEqual(len(dbs), 0)
+
+    def test_timestream_database_delete_force(self):
+        session_factory = self.replay_flight_data('test_timestream_database_delete_force')
+        p = self.load_policy(
+            {
+                'name': 'test-timestream-db-delete-force',
+                'resource': 'aws.timestream-database',
+                'filters': [
+                    {"TableCount": 1}
+                ],
+                'actions': [
+                    {
+                        'type': 'delete',
+                        'force': True
+                    }
+                ]
+            }, session_factory=session_factory
+        )
+        resources = p.run()
+        self.assertEqual(len(resources), 1)
+        client = session_factory().client('timestream-write')
+        dbs = client.list_databases()['Databases']
+        self.assertEqual(len(dbs), 0)
+
+
+class TestTimestreamTable(BaseTest):
+    def test_timestream_table_tag(self):
+        session_factory = self.replay_flight_data('test_timestream_table_tag')
+        p = self.load_policy(
+            {
+                'name': 'test-timestream-table-tag',
+                'resource': 'aws.timestream-table',
+                'filters': [
+                    {
+                        'tag:foo': 'absent'
+                    }
+                ],
+                'actions': [
+                    {
+                        'type': 'tag',
+                        'tags': {'foo': 'bar'}
+                    }
+                ]
+            }, session_factory=session_factory
+        )
+        resources = p.run()
+        self.assertEqual(len(resources), 1)
+        client = session_factory().client('timestream-write')
+        tags = client.list_tags_for_resource(ResourceARN=resources[0]['Arn'])['Tags']
+        self.assertEqual(len(tags), 1)
+        self.assertEqual(tags[0]['Key'], 'foo')
+        self.assertEqual(tags[0]['Value'], 'bar')
+
+    def test_timestream_table_remove_tag(self):
+        session_factory = self.replay_flight_data('test_timestream_table_remove_tag')
+        p = self.load_policy(
+            {
+                'name': 'test-timestream-table-tag',
+                'resource': 'aws.timestream-table',
+                'filters': [
+                    {
+                        'tag:foo': 'present'
+                    }
+                ],
+                'actions': [
+                    {
+                        'type': 'remove-tag',
+                        'tags': ['foo']
+                    }
+                ]
+            }, session_factory=session_factory
+        )
+        resources = p.run()
+        self.assertEqual(len(resources), 1)
+        client = session_factory().client('timestream-write')
+        tags = client.list_tags_for_resource(ResourceARN=resources[0]['Arn'])['Tags']
+        self.assertEqual(len(tags), 0)
+
+    def test_timestream_table_delete(self):
+        session_factory = self.replay_flight_data('test_timestream_table_delete')
+        p = self.load_policy(
+            {
+                'name': 'test-timestream-table-delete',
+                'resource': 'aws.timestream-table',
+                'actions': [
+                    {
+                        'type': 'delete',
+                    }
+                ]
+            }, session_factory=session_factory
+        )
+        resources = p.run()
+        self.assertEqual(len(resources), 1)
+        client = session_factory().client('timestream-write')
+        tables = client.list_tables()['Tables']
+        self.assertEqual(len(tables), 0)