diff --git a/slack_sdk/models/blocks/__init__.py b/slack_sdk/models/blocks/__init__.py index 6a26ed958..5cb4f0772 100644 --- a/slack_sdk/models/blocks/__init__.py +++ b/slack_sdk/models/blocks/__init__.py @@ -68,6 +68,7 @@ CarouselBlock, ContextActionsBlock, ContextBlock, + DataTableBlock, DividerBlock, FileBlock, HeaderBlock, @@ -139,6 +140,7 @@ "CarouselBlock", "ContextActionsBlock", "ContextBlock", + "DataTableBlock", "DividerBlock", "FileBlock", "HeaderBlock", diff --git a/slack_sdk/models/blocks/blocks.py b/slack_sdk/models/blocks/blocks.py index db4de1f3a..1fcf41d51 100644 --- a/slack_sdk/models/blocks/blocks.py +++ b/slack_sdk/models/blocks/blocks.py @@ -108,6 +108,8 @@ def parse(cls, block: Union[dict, "Block"]) -> Optional["Block"]: return AlertBlock(**block) elif type == CarouselBlock.type: return CarouselBlock(**block) + elif type == DataTableBlock.type: + return DataTableBlock(**block) else: cls.logger.warning(f"Unknown block detected and skipped ({block})") return None @@ -1030,3 +1032,63 @@ def _validate_elements_present(self): @JsonValidator(f"elements attribute cannot exceed {elements_max_length} cards") def _validate_elements_length(self): return self.elements is None or len(self.elements) <= self.elements_max_length + + +class DataTableBlock(Block): + type = "data_table" + rows_max_length = 101 + columns_max_length = 20 + page_size_min = 1 + page_size_max = 100 + + @property + def attributes(self) -> Set[str]: # type: ignore[override] + return super().attributes.union({"rows", "caption", "page_size", "row_header_column_index"}) + + def __init__( + self, + *, + rows: Sequence[Sequence[Dict[str, Any]]], + caption: str, + page_size: Optional[int] = None, + row_header_column_index: Optional[int] = None, + block_id: Optional[str] = None, + **others: dict, + ): + """Displays structured, paginated data in a table with a required caption. + https://docs.slack.dev/reference/block-kit/blocks/data-table-block + + Args: + rows (required): An array consisting of table rows. Minimum 2 rows (header plus one data row) + and maximum 101 rows (header plus 100 data rows). All rows must have an identical column + count, with a maximum of 20 columns. Each cell has a type of raw_text, raw_number, or + rich_text. The total character limit across all cells is 10,000. + caption (required): A caption for the table; used as the value for the HTML caption element. + page_size: The number of rows to show per page. Min 1, Max 100. Defaults to 5 if omitted. + row_header_column_index: The 0-based index of the column that uniquely identifies each row + (the row header). Defaults to 0 if omitted. + block_id: A unique identifier for a block. If not specified, a block_id will be generated. + You can use this block_id when you receive an interaction payload to identify the source + of the action. Maximum length for this field is 255 characters. + block_id should be unique for each message and each iteration of a message. + If a message is updated, use a new block_id. + """ + super().__init__(type=self.type, block_id=block_id) + show_unknown_key_warning(self, others) + + self.rows = rows + self.caption = caption + self.page_size = page_size + self.row_header_column_index = row_header_column_index + + @JsonValidator("rows attribute must be specified") + def _validate_rows(self): + return self.rows is not None and len(self.rows) > 0 + + @JsonValidator("caption attribute must be specified") + def _validate_caption(self): + return self.caption is not None + + @JsonValidator(f"page_size must be between {page_size_min} and {page_size_max}") + def _validate_page_size(self): + return self.page_size is None or self.page_size_min <= self.page_size <= self.page_size_max diff --git a/tests/slack_sdk/models/test_blocks.py b/tests/slack_sdk/models/test_blocks.py index fc9ff3266..22de85b7d 100644 --- a/tests/slack_sdk/models/test_blocks.py +++ b/tests/slack_sdk/models/test_blocks.py @@ -12,6 +12,7 @@ CarouselBlock, ContextActionsBlock, ContextBlock, + DataTableBlock, DividerBlock, FileBlock, HeaderBlock, @@ -1556,6 +1557,81 @@ def test_with_raw_text_object_helper(self): self.assertDictEqual(expected, block.to_dict()) +class DataTableBlockTests(unittest.TestCase): + def test_document(self): + """Test basic data table block from Slack documentation example""" + input = { + "type": "data_table", + "caption": "Quarterly sales by region", + "rows": [ + [{"type": "raw_text", "text": "Region"}, {"type": "raw_text", "text": "Sales"}], + [{"type": "raw_text", "text": "West"}, {"type": "raw_number", "value": 120, "text": "120"}], + [{"type": "raw_text", "text": "East"}, {"type": "raw_number", "value": 95, "text": "95"}], + ], + } + self.assertDictEqual(input, DataTableBlock(**input).to_dict()) + self.assertDictEqual(input, Block.parse(input).to_dict()) + + def test_all_fields(self): + """Test data table block with every optional field set""" + input = { + "type": "data_table", + "block_id": "data-table-123", + "caption": "User directory", + "page_size": 25, + "row_header_column_index": 1, + "rows": [ + [{"type": "raw_text", "text": "ID"}, {"type": "raw_text", "text": "Name"}], + [{"type": "raw_number", "value": 1, "text": "1"}, {"type": "raw_text", "text": "Alice"}], + ], + } + self.assertDictEqual(input, DataTableBlock(**input).to_dict()) + self.assertDictEqual(input, Block.parse(input).to_dict()) + + def test_with_rich_text(self): + """Test data table block with rich_text cells""" + input = { + "type": "data_table", + "caption": "Links", + "rows": [ + [{"type": "raw_text", "text": "Site"}], + [ + { + "type": "rich_text", + "elements": [ + { + "type": "rich_text_section", + "elements": [{"text": "Slack", "type": "link", "url": "https://slack.com"}], + } + ], + }, + ], + ], + } + self.assertDictEqual(input, DataTableBlock(**input).to_dict()) + self.assertDictEqual(input, Block.parse(input).to_dict()) + + def test_rows_validation(self): + """Test that empty rows fail validation""" + with self.assertRaises(SlackObjectFormationError): + DataTableBlock(caption="empty", rows=[]).to_dict() + + def test_caption_required(self): + """Test that DataTableBlock requires a caption argument""" + with self.assertRaises(TypeError): + DataTableBlock(rows=[[{"type": "raw_text", "text": "A"}]]) + + def test_page_size_validation(self): + """Test that page_size outside the allowed range fails validation""" + rows = [[{"type": "raw_text", "text": "A"}], [{"type": "raw_text", "text": "B"}]] + with self.assertRaises(SlackObjectFormationError): + DataTableBlock(caption="too small", rows=rows, page_size=0).to_dict() + with self.assertRaises(SlackObjectFormationError): + DataTableBlock(caption="too big", rows=rows, page_size=101).to_dict() + # A valid page_size should pass + DataTableBlock(caption="ok", rows=rows, page_size=50).to_dict() + + class CardBlockTests(unittest.TestCase): def test_document(self): input = {