Repository: slackapi/python-slack-sdk (python)

Issue:
Add rich_text classes to slack_sdk.models module
I'm currently refactoring code using `slack_sdk` models, and most of my modals use `RichText` objects. I've noticed while available in Block Kit Builder and documented thoroughly, there is not a model for it. I created my own (I'd share, but the way it's constructed is significantly different than how I see it done in this project), but when I try to pass this through the `View()` constructor, it skips those blocks because it doesn't recognize the type, despite how well established and integrated into documentation that component appears to be.

I'm wondering if there is a plan to add a model for this in place, and if so, where that is on the priority scale? Also hoping to get clarity on what appears should be `LinkTextObject` for use inside a section or list of a `RichTextElement`. Passing `subtype="link"` to TextObject "works", but the url property gets discarded. 

### Category (place an `x` in each of the `[ ]`)

- [ ] **slack_sdk.web.WebClient (sync/async)** (Web API client)
- [ ] **slack_sdk.webhook.WebhookClient (sync/async)** (Incoming Webhook, response_url sender)
- [x] **slack_sdk.models** (UI component builders)
- [ ] **slack_sdk.oauth** (OAuth Flow Utilities)
- [ ] **slack_sdk.socket_mode** (Socket Mode client)
- [ ] **slack_sdk.audit_logs** (Audit Logs API client)
- [ ] **slack_sdk.scim** (SCIM API client)
- [ ] **slack_sdk.rtm** (RTM client)
- [ ] **slack_sdk.signature** (Request Signature Verifier)

### Requirements

Please read the [Contributing guidelines](https://github.com/slackapi/python-slack-sdk/blob/main/.github/contributing.md) and [Code of Conduct](https://slackhq.github.io/code-of-conduct) before creating this issue or pull request. By submitting, you are agreeing to those rules.


Pull request:
Fix #1428 Add rich_text classes to slack_sdk.models module
## Summary

This pull request resolves #1428 

### Category (place an `x` in each of the `[ ]`)

- [ ] **slack_sdk.web.WebClient (sync/async)** (Web API client)
- [ ] **slack_sdk.webhook.WebhookClient (sync/async)** (Incoming Webhook, response_url sender)
- [ ] **slack_sdk.socket_mode** (Socket Mode client)
- [ ] **slack_sdk.signature** (Request Signature Verifier)
- [ ] **slack_sdk.oauth** (OAuth Flow Utilities)
- [x] **slack_sdk.models** (UI component builders)
- [ ] **slack_sdk.scim** (SCIM API client)
- [ ] **slack_sdk.audit_logs** (Audit Logs API client)
- [ ] **slack_sdk.rtm_v2** (RTM client)
- [ ] `/docs-src` (Documents, have you run `./scripts/docs.sh`?)
- [ ] `/docs-src-v2` (Documents, have you run `./scripts/docs-v2.sh`?)
- [ ] `/tutorial` (PythOnBoardingBot tutorial)
- [ ] `tests`/`integration_tests` (Automated tests for this library)

## Requirements (place an `x` in each `[ ]`)

- [x] I've read and understood the [Contributing Guidelines](https://github.com/slackapi/python-slack-sdk/blob/main/.github/contributing.md) and have done my best effort to follow them.
- [x] I've read and agree to the [Code of Conduct](https://slackhq.github.io/code-of-conduct).
- [x] I've run `python3 -m venv .venv && source .venv/bin/activate && ./scripts/run_validation.sh` after making the changes.


Patch:
diff --git a/slack_sdk/models/blocks/__init__.py b/slack_sdk/models/blocks/__init__.py
index 87febae..96ecfb9 100644
--- a/slack_sdk/models/blocks/__init__.py
+++ b/slack_sdk/models/blocks/__init__.py
@@ -43,6 +43,12 @@ from .block_elements import StaticMultiSelectElement
 from .block_elements import StaticSelectElement
 from .block_elements import UserMultiSelectElement
 from .block_elements import UserSelectElement
+from .block_elements import RichTextElement
+from .block_elements import RichTextElementParts
+from .block_elements import RichTextListElement
+from .block_elements import RichTextPreformattedElement
+from .block_elements import RichTextQuoteElement
+from .block_elements import RichTextSectionElement
 from .blocks import ActionsBlock
 from .blocks import Block
 from .blocks import CallBlock
@@ -54,6 +60,7 @@ from .blocks import ImageBlock
 from .blocks import InputBlock
 from .blocks import SectionBlock
 from .blocks import VideoBlock
+from .blocks import RichTextBlock
 
 __all__ = [
     "ButtonStyles",
@@ -93,6 +100,12 @@ __all__ = [
     "StaticSelectElement",
     "UserMultiSelectElement",
     "UserSelectElement",
+    "RichTextElement",
+    "RichTextElementParts",
+    "RichTextListElement",
+    "RichTextPreformattedElement",
+    "RichTextQuoteElement",
+    "RichTextSectionElement",
     "ActionsBlock",
     "Block",
     "CallBlock",
@@ -104,4 +117,5 @@ __all__ = [
     "InputBlock",
     "SectionBlock",
     "VideoBlock",
+    "RichTextBlock",
 ]
diff --git a/slack_sdk/models/blocks/block_elements.py b/slack_sdk/models/blocks/block_elements.py
index 8b9ac4e..bf2518f 100644
--- a/slack_sdk/models/blocks/block_elements.py
+++ b/slack_sdk/models/blocks/block_elements.py
@@ -1834,3 +1834,309 @@ class WorkflowButtonElement(InteractiveElement):
         self.workflow = workflow
         self.style = style
         self.accessibility_label = accessibility_label
+
+
+# -------------------------------------------------
+# Rich text elements
+# -------------------------------------------------
+
+
+class RichTextElement(BlockElement):
+    pass
+
+
+class RichTextListElement(RichTextElement):
+    type = "rich_text_list"
+
+    @property
+    def attributes(self) -> Set[str]:
+        return super().attributes.union({"elements", "style", "indent", "offset", "border"})
+
+    def __init__(
+        self,
+        *,
+        elements: Sequence[Union[dict, RichTextElement]],
+        style: Optional[str] = None,  # bullet, ordered
+        indent: Optional[int] = None,
+        offset: Optional[int] = None,
+        border: Optional[int] = None,
+        **others: dict,
+    ):
+        super().__init__(type=self.type)
+        show_unknown_key_warning(self, others)
+        self.elements = elements
+        self.style = style
+        self.indent = indent
+        self.offset = offset
+        self.border = border
+
+
+class RichTextPreformattedElement(RichTextElement):
+    type = "rich_text_preformatted"
+
+    @property
+    def attributes(self) -> Set[str]:
+        return super().attributes.union({"elements", "border"})
+
+    def __init__(
+        self,
+        *,
+        elements: Sequence[Union[dict, RichTextElement]],
+        border: Optional[int] = None,
+        **others: dict,
+    ):
+        super().__init__(type=self.type)
+        show_unknown_key_warning(self, others)
+        self.elements = elements
+        self.border = border
+
+
+class RichTextQuoteElement(RichTextElement):
+    type = "rich_text_quote"
+
+    @property
+    def attributes(self) -> Set[str]:
+        return super().attributes.union({"elements"})
+
+    def __init__(
+        self,
+        *,
+        elements: Sequence[Union[dict, RichTextElement]],
+        **others: dict,
+    ):
+        super().__init__(type=self.type)
+        show_unknown_key_warning(self, others)
+        self.elements = elements
+
+
+class RichTextSectionElement(RichTextElement):
+    type = "rich_text_section"
+
+    @property
+    def attributes(self) -> Set[str]:
+        return super().attributes.union({"elements"})
+
+    def __init__(
+        self,
+        *,
+        elements: Sequence[Union[dict, RichTextElement]],
+        **others: dict,
+    ):
+        super().__init__(type=self.type)
+        show_unknown_key_warning(self, others)
+        self.elements = elements
+
+
+class RichTextElementParts:
+    class TextStyle:
+        def __init__(
+            self,
+            *,
+            bold: Optional[bool] = None,
+            italic: Optional[bool] = None,
+            strike: Optional[bool] = None,
+            code: Optional[bool] = None,
+        ):
+            self.bold = bold
+            self.italic = italic
+            self.strike = strike
+            self.code = code
+
+        def to_dict(self, *args) -> dict:
+            result = {
+                "bold": self.bold,
+                "italic": self.italic,
+                "strike": self.strike,
+                "code": self.code,
+            }
+            return {k: v for k, v in result.items() if v is not None}
+
+    class Text(RichTextElement):
+        type = "text"
+
+        @property
+        def attributes(self) -> Set[str]:
+            return super().attributes.union({"text", "style"})
+
+        def __init__(
+            self,
+            *,
+            text: str,
+            style: Optional[Union[dict, "RichTextElementParts.TextStyle"]] = None,
+            **others: dict,
+        ):
+            super().__init__(type=self.type)
+            show_unknown_key_warning(self, others)
+            self.text = text
+            self.style = style
+
+    class Channel(RichTextElement):
+        type = "channel"
+
+        @property
+        def attributes(self) -> Set[str]:
+            return super().attributes.union({"channel_id", "style"})
+
+        def __init__(
+            self,
+            *,
+            channel_id: str,
+            style: Optional[Union[dict, "RichTextElementParts.TextStyle"]] = None,
+            **others: dict,
+        ):
+            super().__init__(type=self.type)
+            show_unknown_key_warning(self, others)
+            self.channel_id = channel_id
+            self.style = style
+
+    class User(RichTextElement):
+        type = "user"
+
+        @property
+        def attributes(self) -> Set[str]:
+            return super().attributes.union({"user_id", "style"})
+
+        def __init__(
+            self,
+            *,
+            user_id: str,
+            style: Optional[Union[dict, "RichTextElementParts.TextStyle"]] = None,
+            **others: dict,
+        ):
+            super().__init__(type=self.type)
+            show_unknown_key_warning(self, others)
+            self.user_id = user_id
+            self.style = style
+
+    class Emoji(RichTextElement):
+        type = "emoji"
+
+        @property
+        def attributes(self) -> Set[str]:
+            return super().attributes.union({"name", "skin_tone", "unicode", "style"})
+
+        def __init__(
+            self,
+            *,
+            name: str,
+            skin_tone: Optional[int] = None,
+            unicode: Optional[str] = None,
+            style: Optional[Union[dict, "RichTextElementParts.TextStyle"]] = None,
+            **others: dict,
+        ):
+            super().__init__(type=self.type)
+            show_unknown_key_warning(self, others)
+            self.name = name
+            self.skin_tone = skin_tone
+            self.unicode = unicode
+            self.style = style
+
+    class Link(RichTextElement):
+        type = "link"
+
+        @property
+        def attributes(self) -> Set[str]:
+            return super().attributes.union({"url", "text", "style"})
+
+        def __init__(
+            self,
+            *,
+            url: str,
+            text: Optional[str] = None,
+            style: Optional[Union[dict, "RichTextElementParts.TextStyle"]] = None,
+            **others: dict,
+        ):
+            super().__init__(type=self.type)
+            show_unknown_key_warning(self, others)
+            self.url = url
+            self.text = text
+            self.style = style
+
+    class Team(RichTextElement):
+        type = "team"
+
+        @property
+        def attributes(self) -> Set[str]:
+            return super().attributes.union({"team_id", "style"})
+
+        def __init__(
+            self,
+            *,
+            team_id: str,
+            style: Optional[Union[dict, "RichTextElementParts.TextStyle"]] = None,
+            **others: dict,
+        ):
+            super().__init__(type=self.type)
+            show_unknown_key_warning(self, others)
+            self.team_id = team_id
+            self.style = style
+
+    class UserGroup(RichTextElement):
+        type = "usergroup"
+
+        @property
+        def attributes(self) -> Set[str]:
+            return super().attributes.union({"usergroup_id", "style"})
+
+        def __init__(
+            self,
+            *,
+            usergroup_id: str,
+            style: Optional[Union[dict, "RichTextElementParts.TextStyle"]] = None,
+            **others: dict,
+        ):
+            super().__init__(type=self.type)
+            show_unknown_key_warning(self, others)
+            self.usergroup_id = usergroup_id
+            self.style = style
+
+    class Date(RichTextElement):
+        type = "date"
+
+        @property
+        def attributes(self) -> Set[str]:
+            return super().attributes.union({"timestamp"})
+
+        def __init__(
+            self,
+            *,
+            timestamp: str,
+            **others: dict,
+        ):
+            super().__init__(type=self.type)
+            show_unknown_key_warning(self, others)
+            self.timestamp = timestamp
+
+    class Broadcast(RichTextElement):
+        type = "broadcast"
+
+        @property
+        def attributes(self) -> Set[str]:
+            return super().attributes.union({"range"})
+
+        def __init__(
+            self,
+            *,
+            range: str,  # channel, here, ..
+            **others: dict,
+        ):
+            super().__init__(type=self.type)
+            show_unknown_key_warning(self, others)
+            self.range = range
+
+    class Color(RichTextElement):
+        type = "color"
+
+        @property
+        def attributes(self) -> Set[str]:
+            return super().attributes.union({"value"})
+
+        def __init__(
+            self,
+            *,
+            value: str,
+            **others: dict,
+        ):
+            super().__init__(type=self.type)
+            show_unknown_key_warning(self, others)
+            self.value = value
diff --git a/slack_sdk/models/blocks/blocks.py b/slack_sdk/models/blocks/blocks.py
index 590f8ae..5bd0171 100644
--- a/slack_sdk/models/blocks/blocks.py
+++ b/slack_sdk/models/blocks/blocks.py
@@ -11,7 +11,7 @@ from slack_sdk.models.basic_objects import (
 from .basic_components import MarkdownTextObject
 from .basic_components import PlainTextObject
 from .basic_components import TextObject
-from .block_elements import BlockElement
+from .block_elements import BlockElement, RichTextElement
 from .block_elements import ImageElement
 from .block_elements import InputInteractiveElement
 from .block_elements import InteractiveElement
@@ -604,3 +604,34 @@ class VideoBlock(Block):
     @JsonValidator(f"author_name attribute cannot exceed {author_name_max_length} characters")
     def _validate_author_name_length(self):
         return self.author_name is None or len(self.author_name) < self.author_name_max_length
+
+
+class RichTextBlock(Block):
+    type = "rich_text"
+
+    @property
+    def attributes(self) -> Set[str]:
+        return super().attributes.union({"elements"})
+
+    def __init__(
+        self,
+        *,
+        elements: Sequence[Union[dict, RichTextElement]],
+        block_id: Optional[str] = None,
+        **others: dict,
+    ):
+        """A block that is used to hold interactive elements.
+        https://api.slack.com/reference/block-kit/blocks#rich_text
+
+        Args:
+            elements (required): An array of rich text objects -
+                rich_text_section, rich_text_list, rich_text_quote, rich_text_preformatted
+            block_id: A unique identifier for a block. If not specified, one will be generated.
+                Maximum length for this field is 255 characters.
+                block_id should be unique for each message or view and each iteration of a message or view.
+                If a message or view is updated, use a new block_id.
+        """
+        super().__init__(type=self.type, block_id=block_id)
+        show_unknown_key_warning(self, others)
+
+        self.elements = BlockElement.parse_all(elements)


Test patch:
diff --git a/tests/slack_sdk/models/test_blocks.py b/tests/slack_sdk/models/test_blocks.py
index 5461ba8..4ea1158 100644
--- a/tests/slack_sdk/models/test_blocks.py
+++ b/tests/slack_sdk/models/test_blocks.py
@@ -22,6 +22,12 @@ from slack_sdk.models.blocks import (
     HeaderBlock,
     VideoBlock,
     Option,
+    RichTextBlock,
+    RichTextSectionElement,
+    RichTextListElement,
+    RichTextQuoteElement,
+    RichTextPreformattedElement,
+    RichTextElementParts,
 )
 from . import STRING_3001_CHARS
 
@@ -825,3 +831,219 @@ class VideoBlockTests(unittest.TestCase):
         }
         with self.assertRaises(SlackObjectFormationError):
             VideoBlock(**input).validate_json()
+
+
+# ----------------------------------------------
+# RichTextBlock
+# ----------------------------------------------
+
+
+class RichTextBlockTests(unittest.TestCase):
+    def test_document(self):
+        inputs = [
+            {
+                "type": "rich_text",
+                "elements": [
+                    {
+                        "type": "rich_text_section",
+                        "elements": [{"type": "text", "text": "Hello there, I am a basic rich text block!"}],
+                    }
+                ],
+            },
+            {
+                "type": "rich_text",
+                "elements": [
+                    {
+                        "type": "rich_text_section",
+                        "elements": [
+                            {"type": "text", "text": "Hello there, "},
+                            {"type": "text", "text": "I am a bold rich text block!", "style": {"bold": True}},
+                        ],
+                    }
+                ],
+            },
+            {
+                "type": "rich_text",
+                "elements": [
+                    {
+                        "type": "rich_text_section",
+                        "elements": [
+                            {"type": "text", "text": "Hello there, "},
+                            {"type": "text", "text": "I am an italic rich text block!", "style": {"italic": True}},
+                        ],
+                    }
+                ],
+            },
+            {
+                "type": "rich_text",
+                "elements": [
+                    {
+                        "type": "rich_text_section",
+                        "elements": [
+                            {"type": "text", "text": "Hello there, "},
+                            {"type": "text", "text": "I am a strikethrough rich text block!", "style": {"strike": True}},
+                        ],
+                    }
+                ],
+            },
+        ]
+        for input in inputs:
+            self.assertDictEqual(input, RichTextBlock(**input).to_dict())
+
+    def test_complex(self):
+        self.maxDiff = None
+        dict_block = {
+            "type": "rich_text",
+            "block_id": "3Uk3Q",
+            "elements": [
+                {
+                    "type": "rich_text_section",
+                    "elements": [
+                        {"type": "text", "text": "Hey!", "style": {"bold": True}},
+                        {"type": "text", "text": " this is "},
+                        {"type": "text", "text": "very", "style": {"strike": True}},
+                        {"type": "text", "text": " rich text "},
+                        {"type": "text", "text": "block", "style": {"code": True}},
+                        {"type": "text", "text": " "},
+                        {"type": "text", "text": "test", "style": {"italic": True}},
+                        {"type": "link", "url": "https://slack.com", "text": "Slack website!"},
+                    ],
+                },
+                {
+                    "type": "rich_text_list",
+                    "elements": [
+                        {"type": "rich_text_section", "elements": [{"type": "text", "text": "a"}]},
+                        {"type": "rich_text_section", "elements": [{"type": "text", "text": "b"}]},
+                    ],
+                    "style": "ordered",
+                    "indent": 0,
+                    "border": 0,
+                },
+                {
+                    "type": "rich_text_list",
+                    "elements": [{"type": "rich_text_section", "elements": [{"type": "text", "text": "bb"}]}],
+                    "style": "ordered",
+                    "indent": 1,
+                    "border": 0,
+                },
+                {
+                    "type": "rich_text_list",
+                    "elements": [{"type": "rich_text_section", "elements": [{"type": "text", "text": "BBB"}]}],
+                    "style": "ordered",
+                    "indent": 2,
+                    "border": 0,
+                },
+                {
+                    "type": "rich_text_list",
+                    "elements": [{"type": "rich_text_section", "elements": [{"type": "text", "text": "c"}]}],
+                    "style": "ordered",
+                    "indent": 0,
+                    "offset": 2,
+                    "border": 0,
+                },
+                {"type": "rich_text_section", "elements": [{"type": "text", "text": "\n"}]},
+                {
+                    "type": "rich_text_list",
+                    "elements": [
+                        {"type": "rich_text_section", "elements": [{"type": "text", "text": "todo"}]},
+                        {"type": "rich_text_section", "elements": [{"type": "text", "text": "todo"}]},
+                        {"type": "rich_text_section", "elements": [{"type": "text", "text": "todo"}]},
+                    ],
+                    "style": "bullet",
+                    "indent": 0,
+                    "border": 0,
+                },
+                {"type": "rich_text_section", "elements": [{"type": "text", "text": "\n"}]},
+                {"type": "rich_text_quote", "elements": [{"type": "text", "text": "this is very important"}]},
+                {
+                    "type": "rich_text_preformatted",
+                    "elements": [{"type": "text", "text": 'print("Hello world")'}],
+                    "border": 0,
+                },
+                {
+                    "type": "rich_text_section",
+                    "elements": [
+                        {"type": "user", "user_id": "WJC6QG0MS"},
+                        {"type": "text", "text": " "},
+                        {"type": "usergroup", "usergroup_id": "S01BL602YLU"},
+                        {"type": "text", "text": " "},
+                        {"type": "channel", "channel_id": "C02GD0YEHDJ"},
+                    ],
+                },
+            ],
+        }
+        self.assertDictEqual(dict_block, RichTextBlock(**dict_block).to_dict())
+
+        _ = RichTextElementParts
+        class_block = RichTextBlock(
+            block_id="3Uk3Q",
+            elements=[
+                RichTextSectionElement(
+                    elements=[
+                        _.Text(text="Hey!", style=_.TextStyle(bold=True)),
+                        _.Text(text=" this is "),
+                        _.Text(text="very", style=_.TextStyle(strike=True)),
+                        _.Text(text=" rich text "),
+                        _.Text(text="block", style=_.TextStyle(code=True)),
+                        _.Text(text=" "),
+                        _.Text(text="test", style=_.TextStyle(italic=True)),
+                        _.Link(text="Slack website!", url="https://slack.com"),
+                    ]
+                ),
+                RichTextListElement(
+                    elements=[
+                        RichTextSectionElement(elements=[_.Text(text="a")]),
+                        RichTextSectionElement(elements=[_.Text(text="b")]),
+                    ],
+                    style="ordered",
+                    indent=0,
+                    border=0,
+                ),
+                RichTextListElement(
+                    elements=[RichTextSectionElement(elements=[_.Text(text="bb")])],
+                    style="ordered",
+                    indent=1,
+                    border=0,
+                ),
+                RichTextListElement(
+                    elements=[RichTextSectionElement(elements=[_.Text(text="BBB")])],
+                    style="ordered",
+                    indent=2,
+                    border=0,
+                ),
+                RichTextListElement(
+                    elements=[RichTextSectionElement(elements=[_.Text(text="c")])],
+                    style="ordered",
+                    indent=0,
+                    offset=2,
+                    border=0,
+                ),
+                RichTextSectionElement(elements=[_.Text(text="\n")]),
+                RichTextListElement(
+                    elements=[
+                        RichTextSectionElement(elements=[_.Text(text="todo")]),
+                        RichTextSectionElement(elements=[_.Text(text="todo")]),
+                        RichTextSectionElement(elements=[_.Text(text="todo")]),
+                    ],
+                    style="bullet",
+                    indent=0,
+                    border=0,
+                ),
+                RichTextSectionElement(elements=[_.Text(text="\n")]),
+                RichTextQuoteElement(elements=[_.Text(text="this is very important")]),
+                RichTextPreformattedElement(
+                    elements=[_.Text(text='print("Hello world")')],
+                    border=0,
+                ),
+                RichTextSectionElement(
+                    elements=[
+                        _.User(user_id="WJC6QG0MS"),
+                        _.Text(text=" "),
+                        _.UserGroup(usergroup_id="S01BL602YLU"),
+                        _.Text(text=" "),
+                        _.Channel(channel_id="C02GD0YEHDJ"),
+                    ]
+                ),
+            ],
+        )
+        self.assertDictEqual(dict_block, class_block.to_dict())