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

Issue:
Add video block to Block Kit model classes
"video" type blocks are now available in Block Kit.

>Showcase your cool cat videos in a new [video block](https://api.slack.com/reference/block-kit/blocks#video) with [Block Kit](https://api.slack.com/block-kit). Read on to learn more on [how to embed videos in your apps](https://api.slack.com/reference/block-kit/blocks#video).

* https://api.slack.com/changelog
* https://api.slack.com/reference/block-kit/blocks#video

We need to add a new class here:
* https://github.com/slackapi/python-slack-sdk/blob/v3.17.2/slack_sdk/models/blocks/blocks.py
* https://github.com/slackapi/python-slack-sdk/blob/v3.17.2/tests/slack_sdk/models/test_blocks.py

### 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 #1236 Add video block to Block Kit model classes
## Summary

This pull request resolves #1236 

### 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 deea314..bb54235 100644
--- a/slack_sdk/models/blocks/__init__.py
+++ b/slack_sdk/models/blocks/__init__.py
@@ -48,6 +48,7 @@ from .blocks import HeaderBlock
 from .blocks import ImageBlock
 from .blocks import InputBlock
 from .blocks import SectionBlock
+from .blocks import VideoBlock
 
 __all__ = [
     "ButtonStyles",
@@ -92,4 +93,5 @@ __all__ = [
     "ImageBlock",
     "InputBlock",
     "SectionBlock",
+    "VideoBlock",
 ]
diff --git a/slack_sdk/models/blocks/blocks.py b/slack_sdk/models/blocks/blocks.py
index fcbc502..b338a90 100644
--- a/slack_sdk/models/blocks/blocks.py
+++ b/slack_sdk/models/blocks/blocks.py
@@ -86,6 +86,8 @@ class Block(JsonObject):
                     return CallBlock(**block)
                 elif type == HeaderBlock.type:
                     return HeaderBlock(**block)
+                elif type == VideoBlock.type:
+                    return VideoBlock(**block)
                 else:
                     cls.logger.warning(f"Unknown block detected and skipped ({block})")
                     return None
@@ -492,3 +494,100 @@ class HeaderBlock(Block):
     @JsonValidator(f"text attribute cannot exceed {text_max_length} characters")
     def _validate_alt_text_length(self):
         return self.text is None or len(self.text.text) <= self.text_max_length
+
+
+class VideoBlock(Block):
+    type = "video"
+    title_max_length = 200
+    author_name_max_length = 50
+
+    @property
+    def attributes(self) -> Set[str]:
+        return super().attributes.union(
+            {
+                "alt_text",
+                "video_url",
+                "thumbnail_url",
+                "title",
+                "title_url",
+                "description",
+                "provider_icon_url",
+                "provider_name",
+                "author_name",
+            }
+        )
+
+    def __init__(
+        self,
+        *,
+        block_id: Optional[str] = None,
+        alt_text: Optional[str] = None,
+        video_url: Optional[str] = None,
+        thumbnail_url: Optional[str] = None,
+        title: Optional[Union[str, dict, PlainTextObject]] = None,
+        title_url: Optional[str] = None,
+        description: Optional[Union[str, dict, PlainTextObject]] = None,
+        provider_icon_url: Optional[str] = None,
+        provider_name: Optional[str] = None,
+        author_name: Optional[str] = None,
+        **others: dict,
+    ):
+        """A video block is designed to embed videos in all app surfaces
+        (e.g. link unfurls, messages, modals, App Home) —
+        anywhere you can put blocks! To use the video block within your app,
+        you must have the links.embed:write scope.
+        https://api.slack.com/reference/block-kit/blocks#video
+
+        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.
+                block_id should be unique for each message and each iteration of a message.
+                If a message is updated, use a new block_id.
+            alt_text (required): A tooltip for the video. Required for accessibility
+            video_url (required): The URL to be embedded. Must match any existing unfurl domains within the app
+                and point to a HTTPS URL.
+            thumbnail_url (required): The thumbnail image URL
+            title (required): Video title in plain text format. Must be less than 200 characters.
+            title_url: Hyperlink for the title text. Must correspond to the non-embeddable URL for the video.
+                Must go to an HTTPS URL.
+            description: Description for video in plain text format.
+            provider_icon_url: Icon for the video provider - ex. Youtube icon
+            provider_name: The originating application or domain of the video ex. Youtube
+            author_name: Author name to be displayed. Must be less than 50 characters.
+        """
+        super().__init__(type=self.type, block_id=block_id)
+        show_unknown_key_warning(self, others)
+
+        self.alt_text = alt_text
+        self.video_url = video_url
+        self.thumbnail_url = thumbnail_url
+        self.title = TextObject.parse(title, default_type=PlainTextObject.type)
+        self.title_url = title_url
+        self.description = TextObject.parse(description, default_type=PlainTextObject.type)
+        self.provider_icon_url = provider_icon_url
+        self.provider_name = provider_name
+        self.author_name = author_name
+
+    @JsonValidator("alt_text attribute must be specified")
+    def _validate_alt_text(self):
+        return self.alt_text is not None
+
+    @JsonValidator("video_url attribute must be specified")
+    def _validate_video_url(self):
+        return self.video_url is not None
+
+    @JsonValidator("thumbnail_url attribute must be specified")
+    def _validate_thumbnail_url(self):
+        return self.thumbnail_url is not None
+
+    @JsonValidator("title attribute must be specified")
+    def _validate_title(self):
+        return self.title is not None
+
+    @JsonValidator(f"title attribute cannot exceed {title_max_length} characters")
+    def _validate_title_length(self):
+        return self.title is None or len(self.title.text) < self.title_max_length
+
+    @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


Test patch:
diff --git a/tests/slack_sdk/models/test_blocks.py b/tests/slack_sdk/models/test_blocks.py
index 86a717f..fa72bc2 100644
--- a/tests/slack_sdk/models/test_blocks.py
+++ b/tests/slack_sdk/models/test_blocks.py
@@ -20,6 +20,7 @@ from slack_sdk.models.blocks import (
     PlainTextObject,
     MarkdownTextObject,
     HeaderBlock,
+    VideoBlock,
     Option,
 )
 from . import STRING_3001_CHARS
@@ -694,3 +695,79 @@ class HeaderBlockTests(unittest.TestCase):
         }
         with self.assertRaises(SlackObjectFormationError):
             HeaderBlock(**input).validate_json()
+
+
+# ----------------------------------------------
+# Video
+# ----------------------------------------------
+
+
+class VideoBlockTests(unittest.TestCase):
+    def test_document(self):
+        input = {
+            "type": "video",
+            "title": {"type": "plain_text", "text": "How to use Slack.", "emoji": True},
+            "title_url": "https://www.youtube.com/watch?v=RRxQQxiM7AA",
+            "description": {
+                "type": "plain_text",
+                "text": "Slack is a new way to communicate with your team. "
+                "It's faster, better organized and more secure than email.",
+                "emoji": True,
+            },
+            "video_url": "https://www.youtube.com/embed/RRxQQxiM7AA?feature=oembed&autoplay=1",
+            "alt_text": "How to use Slack?",
+            "thumbnail_url": "https://i.ytimg.com/vi/RRxQQxiM7AA/hqdefault.jpg",
+            "author_name": "Arcado Buendia",
+            "provider_name": "YouTube",
+            "provider_icon_url": "https://a.slack-edge.com/80588/img/unfurl_icons/youtube.png",
+        }
+        self.assertDictEqual(input, VideoBlock(**input).to_dict())
+        self.assertDictEqual(input, Block.parse(input).to_dict())
+
+    def test_required(self):
+        input = {
+            "type": "video",
+            "title": {"type": "plain_text", "text": "How to use Slack.", "emoji": True},
+            "video_url": "https://www.youtube.com/embed/RRxQQxiM7AA?feature=oembed&autoplay=1",
+            "alt_text": "How to use Slack?",
+            "thumbnail_url": "https://i.ytimg.com/vi/RRxQQxiM7AA/hqdefault.jpg",
+        }
+        VideoBlock(**input).validate_json()
+
+    def test_required_error(self):
+        # title is missing
+        input = {
+            "type": "video",
+            "video_url": "https://www.youtube.com/embed/RRxQQxiM7AA?feature=oembed&autoplay=1",
+            "alt_text": "How to use Slack?",
+            "thumbnail_url": "https://i.ytimg.com/vi/RRxQQxiM7AA/hqdefault.jpg",
+        }
+        with self.assertRaises(SlackObjectFormationError):
+            VideoBlock(**input).validate_json()
+
+    def test_title_length_199(self):
+        input = {
+            "type": "video",
+            "title": {
+                "type": "plain_text",
+                "text": "1234567890" * 19 + "123456789",
+            },
+            "video_url": "https://www.youtube.com/embed/RRxQQxiM7AA?feature=oembed&autoplay=1",
+            "alt_text": "How to use Slack?",
+            "thumbnail_url": "https://i.ytimg.com/vi/RRxQQxiM7AA/hqdefault.jpg",
+        }
+        VideoBlock(**input).validate_json()
+
+    def test_title_length_200(self):
+        input = {
+            "type": "video",
+            "title": {
+                "type": "plain_text",
+                "text": "1234567890" * 20,
+            },
+            "video_url": "https://www.youtube.com/embed/RRxQQxiM7AA?feature=oembed&autoplay=1",
+            "alt_text": "How to use Slack?",
+            "thumbnail_url": "https://i.ytimg.com/vi/RRxQQxiM7AA/hqdefault.jpg",
+        }
+        with self.assertRaises(SlackObjectFormationError):
+            VideoBlock(**input).validate_json()