From 8df67b4e04170f772316c890f3fbdedd288f230d Mon Sep 17 00:00:00 2001 From: Ale Mercado Date: Wed, 29 Apr 2026 15:48:14 -0400 Subject: [PATCH 1/4] feat(blocks): add new block kit types {Card, Carousel, Alarm} --- slack_sdk/models/blocks/__init__.py | 6 + slack_sdk/models/blocks/blocks.py | 155 ++++++++++++++++++++++++++ tests/slack_sdk/models/test_blocks.py | 153 +++++++++++++++++++++++++ 3 files changed, 314 insertions(+) diff --git a/slack_sdk/models/blocks/__init__.py b/slack_sdk/models/blocks/__init__.py index b8592c2e9..6a26ed958 100644 --- a/slack_sdk/models/blocks/__init__.py +++ b/slack_sdk/models/blocks/__init__.py @@ -61,8 +61,11 @@ ) from .blocks import ( ActionsBlock, + AlertBlock, Block, CallBlock, + CardBlock, + CarouselBlock, ContextActionsBlock, ContextBlock, DividerBlock, @@ -129,8 +132,11 @@ "RichTextQuoteElement", "RichTextSectionElement", "ActionsBlock", + "AlertBlock", "Block", "CallBlock", + "CardBlock", + "CarouselBlock", "ContextActionsBlock", "ContextBlock", "DividerBlock", diff --git a/slack_sdk/models/blocks/blocks.py b/slack_sdk/models/blocks/blocks.py index 7d7f937f5..a5d4b3561 100644 --- a/slack_sdk/models/blocks/blocks.py +++ b/slack_sdk/models/blocks/blocks.py @@ -102,6 +102,12 @@ def parse(cls, block: Union[dict, "Block"]) -> Optional["Block"]: return TaskCardBlock(**block) elif type == PlanBlock.type: return PlanBlock(**block) + elif type == CardBlock.type: + return CardBlock(**block) + elif type == AlertBlock.type: + return AlertBlock(**block) + elif type == CarouselBlock.type: + return CarouselBlock(**block) else: cls.logger.warning(f"Unknown block detected and skipped ({block})") return None @@ -878,3 +884,152 @@ def __init__( self.title = title self.tasks = tasks + + +class CardBlock(Block): + type = "card" + title_max_length = 150 + subtitle_max_length = 150 + body_max_length = 200 + + @property + def attributes(self) -> Set[str]: # type: ignore[override] + return super().attributes.union( + { + "hero_image", + "icon", + "title", + "subtitle", + "body", + "actions", + } + ) + + def __init__( + self, + *, + block_id: Optional[str] = None, + hero_image: Optional[Union[dict, ImageElement]] = None, + icon: Optional[Union[dict, ImageElement]] = None, + title: Optional[Union[str, dict, TextObject]] = None, + subtitle: Optional[Union[str, dict, TextObject]] = None, + body: Optional[Union[str, dict, TextObject]] = None, + actions: Optional[Sequence[Union[dict, BlockElement]]] = None, + **others: dict, + ): + """A rich display block for presenting structured content such as recommendations, results, or work items. + https://docs.slack.dev/reference/block-kit/blocks/card-block + + Args: + block_id: A string acting as a unique identifier for a block. If not specified, one will be generated. + Maximum length for this field is 255 characters. + hero_image: A top banner image for the card. An image element with type, image_url, and alt_text. + icon: A small icon next to the title/subtitle. An image element with type, image_url, and alt_text. + title: The title of the card. Supports mrkdwn text. Maximum length is 150 characters. + subtitle: The subtitle of the card. Supports mrkdwn text. Maximum length is 150 characters. + body: The body text of the card. Supports mrkdwn text. Maximum length is 200 characters. + actions: An array of button elements displayed at the bottom of the card. + """ + super().__init__(type=self.type, block_id=block_id) + show_unknown_key_warning(self, others) + + self.hero_image = BlockElement.parse(hero_image) # type: ignore[arg-type] + self.icon = BlockElement.parse(icon) # type: ignore[arg-type] + self.title = TextObject.parse(title, default_type=MarkdownTextObject.type) # type: ignore[arg-type] + self.subtitle = TextObject.parse(subtitle, default_type=MarkdownTextObject.type) # type: ignore[arg-type] + self.body = TextObject.parse(body, default_type=MarkdownTextObject.type) # type: ignore[arg-type] + self.actions = BlockElement.parse_all(actions) if actions else None # type: ignore[arg-type] + + @JsonValidator("At least one of hero_image, title, actions, or body is required") + def _validate_content(self): + return self.hero_image is not None or self.title is not None or self.actions is not None or self.body is not None + + @JsonValidator(f"title attribute cannot exceed {title_max_length} characters") + def _validate_title_length(self): + return self.title is None or self.title.text is None or len(self.title.text) <= self.title_max_length + + @JsonValidator(f"subtitle attribute cannot exceed {subtitle_max_length} characters") + def _validate_subtitle_length(self): + return self.subtitle is None or self.subtitle.text is None or len(self.subtitle.text) <= self.subtitle_max_length + + @JsonValidator(f"body attribute cannot exceed {body_max_length} characters") + def _validate_body_length(self): + return self.body is None or self.body.text is None or len(self.body.text) <= self.body_max_length + + +class AlertBlock(Block): + type = "alert" + valid_levels = {"default", "info", "warning", "error", "success"} + + @property + def attributes(self) -> Set[str]: # type: ignore[override] + return super().attributes.union({"text", "level"}) + + def __init__( + self, + *, + text: Union[str, dict, TextObject], + level: Optional[str] = None, + block_id: Optional[str] = None, + **others: dict, + ): + """A prominent notice block for displaying warnings, status updates, or other important information. + https://docs.slack.dev/reference/block-kit/blocks/alert-block + + Args: + text (required): The alert message content. Supports plain_text or mrkdwn. + level: The severity level of the alert. One of "default", "info", "warning", "error", "success". + Defaults to "default". + block_id: A string acting as a unique identifier for a block. If not specified, one will be generated. + Maximum length for this field is 255 characters. + """ + super().__init__(type=self.type, block_id=block_id) + show_unknown_key_warning(self, others) + + self.text = TextObject.parse(text) # type: ignore[arg-type] + self.level = level + + @JsonValidator("text attribute must be specified") + def _validate_text(self): + return self.text is not None + + @JsonValidator("level must be a valid value (default, info, warning, error, success)") + def _validate_level(self): + return self.level is None or self.level in self.valid_levels + + +class CarouselBlock(Block): + type = "carousel" + elements_max_length = 10 + + @property + def attributes(self) -> Set[str]: # type: ignore[override] + return super().attributes.union({"elements"}) + + def __init__( + self, + *, + elements: Sequence[Union[dict, "CardBlock"]], + block_id: Optional[str] = None, + **others: dict, + ): + """A horizontally scrollable collection of card blocks. + https://docs.slack.dev/reference/block-kit/blocks/carousel-block + + Args: + elements (required): An array of card block objects. Minimum 1, maximum 10 cards. + block_id: A string acting as a unique identifier for a block. If not specified, one will be generated. + Maximum length for this field is 255 characters. + """ + super().__init__(type=self.type, block_id=block_id) + show_unknown_key_warning(self, others) + + self.elements = Block.parse_all(elements) + + @JsonValidator("elements attribute must contain at least 1 card") + def _validate_elements_present(self): + return self.elements is not None and len(self.elements) >= 1 + + @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 diff --git a/tests/slack_sdk/models/test_blocks.py b/tests/slack_sdk/models/test_blocks.py index 531ebe057..26f5c6305 100644 --- a/tests/slack_sdk/models/test_blocks.py +++ b/tests/slack_sdk/models/test_blocks.py @@ -4,9 +4,12 @@ from slack_sdk.errors import SlackObjectFormationError from slack_sdk.models.blocks import ( ActionsBlock, + AlertBlock, Block, ButtonElement, CallBlock, + CardBlock, + CarouselBlock, ContextActionsBlock, ContextBlock, DividerBlock, @@ -1551,3 +1554,153 @@ def test_with_raw_text_object_helper(self): ], } self.assertDictEqual(expected, block.to_dict()) + + +class CardBlockTests(unittest.TestCase): + def test_document(self): + input = { + "type": "card", + "icon": {"type": "image", "image_url": "https://picsum.photos/36/36", "alt_text": "Icon"}, + "title": {"type": "mrkdwn", "text": "Lumon Industries", "verbatim": False}, + "subtitle": {"type": "mrkdwn", "text": "Committed to work-life balance", "verbatim": False}, + "hero_image": {"type": "image", "image_url": "https://picsum.photos/400/300", "alt_text": "Sample hero image"}, + "body": {"type": "mrkdwn", "text": "Please enjoy each card equally.", "verbatim": False}, + "actions": [ + { + "type": "button", + "text": {"type": "plain_text", "text": "Action Button", "emoji": False}, + "action_id": "button_action", + } + ], + } + self.assertDictEqual(input, CardBlock(**input).to_dict()) + + def test_parse(self): + input = { + "type": "card", + "title": {"type": "mrkdwn", "text": "Title"}, + "body": {"type": "mrkdwn", "text": "Body text"}, + } + parsed = Block.parse(input) + self.assertIsNotNone(parsed) + self.assertDictEqual(input, parsed.to_dict()) + + def test_minimal_with_title(self): + input = { + "type": "card", + "title": {"type": "mrkdwn", "text": "Just a title"}, + } + self.assertDictEqual(input, CardBlock(**input).to_dict()) + + def test_minimal_with_body(self): + input = { + "type": "card", + "body": {"type": "mrkdwn", "text": "Just body text"}, + } + self.assertDictEqual(input, CardBlock(**input).to_dict()) + + def test_validation_at_least_one_field(self): + with self.assertRaises(SlackObjectFormationError): + CardBlock().validate_json() + + def test_title_length_validation(self): + with self.assertRaises(SlackObjectFormationError): + CardBlock(title={"type": "mrkdwn", "text": "a" * 151}).validate_json() + + def test_subtitle_length_validation(self): + with self.assertRaises(SlackObjectFormationError): + CardBlock( + title={"type": "mrkdwn", "text": "Title"}, + subtitle={"type": "mrkdwn", "text": "a" * 151}, + ).validate_json() + + def test_body_length_validation(self): + with self.assertRaises(SlackObjectFormationError): + CardBlock(body={"type": "mrkdwn", "text": "a" * 201}).validate_json() + + +class AlertBlockTests(unittest.TestCase): + def test_document(self): + input = { + "type": "alert", + "text": {"type": "mrkdwn", "text": "The work is mysterious and important.", "verbatim": False}, + "level": "info", + } + self.assertDictEqual(input, AlertBlock(**input).to_dict()) + + def test_parse(self): + input = { + "type": "alert", + "text": {"type": "mrkdwn", "text": "Notice"}, + "level": "warning", + } + parsed = Block.parse(input) + self.assertIsNotNone(parsed) + self.assertDictEqual(input, parsed.to_dict()) + + def test_minimal(self): + input = { + "type": "alert", + "text": {"type": "plain_text", "text": "Simple alert"}, + } + self.assertDictEqual(input, AlertBlock(**input).to_dict()) + + def test_all_levels(self): + for level in ["default", "info", "warning", "error", "success"]: + input = { + "type": "alert", + "text": {"type": "plain_text", "text": "Test"}, + "level": level, + } + AlertBlock(**input).validate_json() + + def test_invalid_level(self): + with self.assertRaises(SlackObjectFormationError): + AlertBlock(text={"type": "plain_text", "text": "Test"}, level="critical").validate_json() + + def test_missing_text(self): + with self.assertRaises(SlackObjectFormationError): + AlertBlock(text="").validate_json() + + +class CarouselBlockTests(unittest.TestCase): + def test_document(self): + input = { + "type": "carousel", + "elements": [ + { + "type": "card", + "title": {"type": "mrkdwn", "text": "Card 1"}, + }, + { + "type": "card", + "title": {"type": "mrkdwn", "text": "Card 2"}, + "body": {"type": "mrkdwn", "text": "Some body text"}, + }, + ], + } + self.assertDictEqual(input, CarouselBlock(**input).to_dict()) + + def test_parse(self): + input = { + "type": "carousel", + "elements": [ + {"type": "card", "title": {"type": "mrkdwn", "text": "Card 1"}}, + ], + } + parsed = Block.parse(input) + self.assertIsNotNone(parsed) + self.assertDictEqual(input, parsed.to_dict()) + + def test_single_card(self): + input = { + "type": "carousel", + "elements": [ + {"type": "card", "title": {"type": "mrkdwn", "text": "Only card"}}, + ], + } + self.assertDictEqual(input, CarouselBlock(**input).to_dict()) + + def test_empty_elements_validation(self): + with self.assertRaises(SlackObjectFormationError): + CarouselBlock(elements=[]).validate_json() From 97deced0c09e44081d59d27026f69a41d99afb28 Mon Sep 17 00:00:00 2001 From: Ale Mercado Date: Wed, 29 Apr 2026 16:00:39 -0400 Subject: [PATCH 2/4] fix: remove unused type ignore comments for mypy --- slack_sdk/models/blocks/blocks.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/slack_sdk/models/blocks/blocks.py b/slack_sdk/models/blocks/blocks.py index a5d4b3561..1670ccf3f 100644 --- a/slack_sdk/models/blocks/blocks.py +++ b/slack_sdk/models/blocks/blocks.py @@ -938,7 +938,7 @@ def __init__( self.title = TextObject.parse(title, default_type=MarkdownTextObject.type) # type: ignore[arg-type] self.subtitle = TextObject.parse(subtitle, default_type=MarkdownTextObject.type) # type: ignore[arg-type] self.body = TextObject.parse(body, default_type=MarkdownTextObject.type) # type: ignore[arg-type] - self.actions = BlockElement.parse_all(actions) if actions else None # type: ignore[arg-type] + self.actions = BlockElement.parse_all(actions) if actions else None @JsonValidator("At least one of hero_image, title, actions, or body is required") def _validate_content(self): @@ -986,7 +986,7 @@ def __init__( super().__init__(type=self.type, block_id=block_id) show_unknown_key_warning(self, others) - self.text = TextObject.parse(text) # type: ignore[arg-type] + self.text = TextObject.parse(text) self.level = level @JsonValidator("text attribute must be specified") From 8acadefad095c21bc0f7142c66337aa8c460dc30 Mon Sep 17 00:00:00 2001 From: Ale Mercado <104795114+srtaalej@users.noreply.github.com> Date: Mon, 4 May 2026 16:33:47 -0400 Subject: [PATCH 3/4] Update slack_sdk/models/blocks/blocks.py Co-authored-by: Eden Zimbelman --- slack_sdk/models/blocks/blocks.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/slack_sdk/models/blocks/blocks.py b/slack_sdk/models/blocks/blocks.py index 1670ccf3f..b557f8912 100644 --- a/slack_sdk/models/blocks/blocks.py +++ b/slack_sdk/models/blocks/blocks.py @@ -917,7 +917,7 @@ def __init__( actions: Optional[Sequence[Union[dict, BlockElement]]] = None, **others: dict, ): - """A rich display block for presenting structured content such as recommendations, results, or work items. + """Displays content in a card.""" https://docs.slack.dev/reference/block-kit/blocks/card-block Args: From 189f6509514643d8596e78f797dc4437a3fdf721 Mon Sep 17 00:00:00 2001 From: Ale Mercado Date: Mon, 4 May 2026 16:48:12 -0400 Subject: [PATCH 4/4] Address PR feedback: alphabetize blocks, align docstrings with docs, fix card field types --- slack_sdk/models/blocks/blocks.py | 115 +++++++++++++------------- tests/slack_sdk/models/test_blocks.py | 4 +- 2 files changed, 58 insertions(+), 61 deletions(-) diff --git a/slack_sdk/models/blocks/blocks.py b/slack_sdk/models/blocks/blocks.py index b557f8912..db4de1f3a 100644 --- a/slack_sdk/models/blocks/blocks.py +++ b/slack_sdk/models/blocks/blocks.py @@ -886,6 +886,46 @@ def __init__( self.tasks = tasks +class AlertBlock(Block): + type = "alert" + valid_levels = {"default", "info", "warning", "error", "success"} + + @property + def attributes(self) -> Set[str]: # type: ignore[override] + return super().attributes.union({"text", "level"}) + + def __init__( + self, + *, + text: Union[str, dict, TextObject], + level: Optional[str] = None, + block_id: Optional[str] = None, + **others: dict, + ): + """Displays alerts, warnings, and informational messages. + https://docs.slack.dev/reference/block-kit/blocks/alert-block + + Args: + text (required): The alert message, using plain_text or mrkdwn formatting. + level: One of "default", "info", "warning", "error", or "success". + Will be "default" if omitted. + block_id: A unique identifier for a block. If not specified, a block_id will be generated. + """ + super().__init__(type=self.type, block_id=block_id) + show_unknown_key_warning(self, others) + + self.text = TextObject.parse(text) + self.level = level + + @JsonValidator("text attribute must be specified") + def _validate_text(self): + return self.text is not None + + @JsonValidator("level must be a valid value (default, info, warning, error, success)") + def _validate_level(self): + return self.level is None or self.level in self.valid_levels + + class CardBlock(Block): type = "card" title_max_length = 150 @@ -909,32 +949,31 @@ def __init__( self, *, block_id: Optional[str] = None, - hero_image: Optional[Union[dict, ImageElement]] = None, - icon: Optional[Union[dict, ImageElement]] = None, + hero_image: Optional[str] = None, + icon: Optional[str] = None, title: Optional[Union[str, dict, TextObject]] = None, subtitle: Optional[Union[str, dict, TextObject]] = None, body: Optional[Union[str, dict, TextObject]] = None, actions: Optional[Sequence[Union[dict, BlockElement]]] = None, **others: dict, ): - """Displays content in a card.""" + """Displays content in a card. https://docs.slack.dev/reference/block-kit/blocks/card-block Args: - block_id: A string acting as a unique identifier for a block. If not specified, one will be generated. - Maximum length for this field is 255 characters. - hero_image: A top banner image for the card. An image element with type, image_url, and alt_text. - icon: A small icon next to the title/subtitle. An image element with type, image_url, and alt_text. - title: The title of the card. Supports mrkdwn text. Maximum length is 150 characters. - subtitle: The subtitle of the card. Supports mrkdwn text. Maximum length is 150 characters. - body: The body text of the card. Supports mrkdwn text. Maximum length is 200 characters. - actions: An array of button elements displayed at the bottom of the card. + block_id: A unique identifier for a block. If not specified, a block_id will be generated. + hero_image: Link to the top image used on the card. + icon: Link to the small image used next to the card's title and subtitle. + title: Title of the card. 150 characters max. + subtitle: Subtitle of the card. 150 characters max. + body: Content of the card. 200 characters max. + actions: Action buttons shown at the bottom of the card. """ super().__init__(type=self.type, block_id=block_id) show_unknown_key_warning(self, others) - self.hero_image = BlockElement.parse(hero_image) # type: ignore[arg-type] - self.icon = BlockElement.parse(icon) # type: ignore[arg-type] + self.hero_image = hero_image + self.icon = icon self.title = TextObject.parse(title, default_type=MarkdownTextObject.type) # type: ignore[arg-type] self.subtitle = TextObject.parse(subtitle, default_type=MarkdownTextObject.type) # type: ignore[arg-type] self.body = TextObject.parse(body, default_type=MarkdownTextObject.type) # type: ignore[arg-type] @@ -957,47 +996,6 @@ def _validate_body_length(self): return self.body is None or self.body.text is None or len(self.body.text) <= self.body_max_length -class AlertBlock(Block): - type = "alert" - valid_levels = {"default", "info", "warning", "error", "success"} - - @property - def attributes(self) -> Set[str]: # type: ignore[override] - return super().attributes.union({"text", "level"}) - - def __init__( - self, - *, - text: Union[str, dict, TextObject], - level: Optional[str] = None, - block_id: Optional[str] = None, - **others: dict, - ): - """A prominent notice block for displaying warnings, status updates, or other important information. - https://docs.slack.dev/reference/block-kit/blocks/alert-block - - Args: - text (required): The alert message content. Supports plain_text or mrkdwn. - level: The severity level of the alert. One of "default", "info", "warning", "error", "success". - Defaults to "default". - block_id: A string acting as a unique identifier for a block. If not specified, one will be generated. - Maximum length for this field is 255 characters. - """ - super().__init__(type=self.type, block_id=block_id) - show_unknown_key_warning(self, others) - - self.text = TextObject.parse(text) - self.level = level - - @JsonValidator("text attribute must be specified") - def _validate_text(self): - return self.text is not None - - @JsonValidator("level must be a valid value (default, info, warning, error, success)") - def _validate_level(self): - return self.level is None or self.level in self.valid_levels - - class CarouselBlock(Block): type = "carousel" elements_max_length = 10 @@ -1009,17 +1007,16 @@ def attributes(self) -> Set[str]: # type: ignore[override] def __init__( self, *, - elements: Sequence[Union[dict, "CardBlock"]], + elements: Sequence[Union[dict, CardBlock]], block_id: Optional[str] = None, **others: dict, ): - """A horizontally scrollable collection of card blocks. + """Displays related card blocks in a horizontally-scrolling container. https://docs.slack.dev/reference/block-kit/blocks/carousel-block Args: - elements (required): An array of card block objects. Minimum 1, maximum 10 cards. - block_id: A string acting as a unique identifier for a block. If not specified, one will be generated. - Maximum length for this field is 255 characters. + elements (required): A list of cards. Minimum 1, maximum 10 cards. + block_id: A unique identifier for a block. If not specified, a block_id will be generated. """ super().__init__(type=self.type, block_id=block_id) show_unknown_key_warning(self, others) diff --git a/tests/slack_sdk/models/test_blocks.py b/tests/slack_sdk/models/test_blocks.py index 26f5c6305..fc9ff3266 100644 --- a/tests/slack_sdk/models/test_blocks.py +++ b/tests/slack_sdk/models/test_blocks.py @@ -1560,10 +1560,10 @@ class CardBlockTests(unittest.TestCase): def test_document(self): input = { "type": "card", - "icon": {"type": "image", "image_url": "https://picsum.photos/36/36", "alt_text": "Icon"}, + "icon": "https://picsum.photos/36/36", "title": {"type": "mrkdwn", "text": "Lumon Industries", "verbatim": False}, "subtitle": {"type": "mrkdwn", "text": "Committed to work-life balance", "verbatim": False}, - "hero_image": {"type": "image", "image_url": "https://picsum.photos/400/300", "alt_text": "Sample hero image"}, + "hero_image": "https://picsum.photos/400/300", "body": {"type": "mrkdwn", "text": "Please enjoy each card equally.", "verbatim": False}, "actions": [ {