Repository: localstack/localstack
Pull request: Handling TableClass in DynamoDB
`TableClass` was not managed for the CRUD operations in DynamoDB. This PR should fix #5916 

Problem statement:
bug: Response object TableDescription contains a member which is not specified: TableClass
### Is there an existing issue for this?

- [X] I have searched the existing issues

### Current Behavior

> ERROR:localstack.aws.protocol.serializer: Response object TableDescription contains a member which is not specified: TableClass
> 
> Traceback (most recent call last):
> 
>   File "/opt/code/localstack/localstack/aws/protocol/serializer.py", line 1007, in _serialize_type_structure
> 
>     member_shape = members[member_key]
> 
> KeyError: 'TableClass'

### Expected Behavior

_No response_

### How are you starting LocalStack?

With a docker-compose file

### Steps To Reproduce

Create a DynamoDB table via Pulumi.

### Environment

```markdown
- OS: Mac M1
- LocalStack: latest
```


### Anything else?

_No response_

Hints from issue discussion:
Welcome to LocalStack! Thanks for reporting your first issue and our team will be working towards fixing the issue for you or reach out for more background information. We recommend joining our [Slack Community](https://localstack.cloud/contact/) for real-time help and drop a message to LocalStack Pro Support if you are a Pro user! If you are willing to contribute towards fixing this issue, please have a look at our [contributing guidelines](https://github.com/localstack/.github/blob/main/CONTRIBUTING.md) and our [developer guide](https://docs.localstack.cloud/developer-guide/).

Commit message: Formatting for linting

Patch to review:
diff --git a/localstack/services/dynamodb/provider.py b/localstack/services/dynamodb/provider.py
index 1d334c2ca3c07..f74c2f92efc70 100644
--- a/localstack/services/dynamodb/provider.py
+++ b/localstack/services/dynamodb/provider.py
@@ -421,6 +421,12 @@ def create_table(
         if "StreamSpecification" in table_definitions:
             create_dynamodb_stream(table_definitions, table_description.get("LatestStreamLabel"))
 
+        if "TableClass" in table_definitions:
+            table_class = table_description.pop("TableClass", None) or table_definitions.pop(
+                "TableClass"
+            )
+            table_description["TableClassSummary"] = {"TableClass": table_class}
+
         tags = table_definitions.pop("Tags", [])
         if tags:
             table_arn = table_description["TableArn"]
@@ -477,6 +483,10 @@ def describe_table(self, context: RequestContext, table_name: TableName) -> Desc
             for key in ["TableId", "SSEDescription"]:
                 if table_definitions.get(key):
                     result.get("Table", {})[key] = table_definitions[key]
+            if "TableClass" in table_definitions:
+                result.get("Table", {})["TableClassSummary"] = {
+                    "TableClass": table_definitions["TableClass"]
+                }
 
         return result
 
@@ -491,8 +501,18 @@ def update_table(
             is_no_update_error = (
                 e.code == "ValidationException" and "Nothing to update" in e.message
             )
-            if is_no_update_error and update_table_input.get("ReplicaUpdates"):
-                table_name = update_table_input.get("TableName")
+            if is_no_update_error and not list(
+                {"TableClass", "ReplicaUpdates"} & set(update_table_input.keys())
+            ):
+                raise
+
+            table_name = update_table_input.get("TableName")
+
+            if update_table_input.get("TableClass"):
+                table_definitions = DynamoDBRegion.get().table_definitions.get(table_name)
+                table_definitions["TableClass"] = update_table_input.get("TableClass")
+
+            if update_table_input.get("ReplicaUpdates"):
                 # update local table props (replicas)
                 table_properties = DynamoDBRegion.get().table_properties
                 table_properties[table_name] = table_props = table_properties.get(table_name) or {}
@@ -511,10 +531,10 @@ def update_table(
                             table_props["Replicas"] = [
                                 r for r in replicas if r.get("RegionName") != region
                             ]
-                # update response content
-                schema = SchemaExtractor.get_table_schema(table_name)
-                return UpdateTableOutput(TableDescription=schema["Table"])
-            raise
+
+            # update response content
+            schema = SchemaExtractor.get_table_schema(table_name)
+            return UpdateTableOutput(TableDescription=schema["Table"])
 
         if "StreamSpecification" in update_table_input:
             create_dynamodb_stream(
diff --git a/tests/integration/test_dynamodb.py b/tests/integration/test_dynamodb.py
index 79640d76937c6..18dd44ad279f4 100644
--- a/tests/integration/test_dynamodb.py
+++ b/tests/integration/test_dynamodb.py
@@ -506,6 +506,31 @@ def wait_for_stream_created(table_name):
         )
         assert "ShardIterator" in response
 
+    def test_dynamodb_create_table_with_class(self, dynamodb_client):
+        table_name = "table_with_class_%s" % short_uid()
+        # create table
+        result = dynamodb_client.create_table(
+            TableName=table_name,
+            KeySchema=[{"AttributeName": "id", "KeyType": "HASH"}],
+            AttributeDefinitions=[{"AttributeName": "id", "AttributeType": "S"}],
+            ProvisionedThroughput={"ReadCapacityUnits": 5, "WriteCapacityUnits": 5},
+            TableClass="STANDARD",
+        )
+        assert result["TableDescription"]["TableClassSummary"]["TableClass"] == "STANDARD"
+        result = dynamodb_client.describe_table(TableName=table_name)
+        assert result["Table"]["TableClassSummary"]["TableClass"] == "STANDARD"
+        result = dynamodb_client.update_table(
+            TableName=table_name, TableClass="STANDARD_INFREQUENT_ACCESS"
+        )
+        assert (
+            result["TableDescription"]["TableClassSummary"]["TableClass"]
+            == "STANDARD_INFREQUENT_ACCESS"
+        )
+        result = dynamodb_client.describe_table(TableName=table_name)
+        assert result["Table"]["TableClassSummary"]["TableClass"] == "STANDARD_INFREQUENT_ACCESS"
+        # clean resources
+        dynamodb_client.delete_table(TableName=table_name)
+
     def test_dynamodb_execute_transaction(self, dynamodb_client):
         table_name = "table_%s" % short_uid()
         dynamodb_client.create_table(


Code review comments:
diff_hunk: @@ -491,8 +501,18 @@ def update_table(
             is_no_update_error = (
                 e.code == "ValidationException" and "Nothing to update" in e.message
             )
-            if is_no_update_error and update_table_input.get("ReplicaUpdates"):
-                table_name = update_table_input.get("TableName")
+            if is_no_update_error and not list(
+                {"TableClass", "ReplicaUpdates"} & set(update_table_input.keys())
+            ):
+                raise
+
+            table_name = update_table_input.get("TableName")
+
+            if update_table_input.get("TableClass"):
+                table_definitions = DynamoDBRegion.get().table_definitions.get(table_name)
path: localstack/services/dynamodb/provider.py
text: @user1:
I think we should hedge against None values here.
Either:
```
table_definitions = DynamoDBRegion.get().table_definitions.setdefault(table_name, {})
```

... or we could define `table_definitions` in `DynamoDBRegion` as a `defaultdict`:
```
self.table_definitions = defaultdict(dict)
```

diff_hunk: @@ -491,8 +501,18 @@ def update_table(
             is_no_update_error = (
                 e.code == "ValidationException" and "Nothing to update" in e.message
             )
-            if is_no_update_error and update_table_input.get("ReplicaUpdates"):
-                table_name = update_table_input.get("TableName")
+            if is_no_update_error and not list(
path: localstack/services/dynamodb/provider.py
text: @user1:
Should this condition be:
```
if not is_no_update_error or not list(...):
    raise
...
```
?

(Nice refactoring btw - this _Return Early_ pattern can help reduce the level of nesting and make the code a bit easier to read. 👍 )