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

Issue:
Add "slack_file" properties to "image" blocks/elements under slack_sdk.models
The "image" blocks and block elements now can have "slack_file", which refers to an uploaded image file within Slack instead of "image_url", which must be a publicly hosted one. The Block Kit class representation in this SDK should add supports for these new options.

References:
* https://api.slack.com/reference/block-kit/blocks#image_fields
* https://api.slack.com/reference/block-kit/block-elements#image
* https://api.slack.com/reference/block-kit/composition-objects#slack_file

### 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 description:
Fix #1450 slack_file in image block/element
## Summary

This pull request resolves #1450 

### 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/pyproject.toml b/pyproject.toml
index 1976da1..a0bf06c 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
 
 [project]
 name = "slack_sdk"
-dynamic = ["version", "readme", "authors"]
+dynamic = ["version", "readme", "authors", "optional-dependencies"]
 description = "The Slack API Platform SDK for Python"
 license = { text = "MIT" }
 requires-python = ">=3.6"
@@ -48,6 +48,7 @@ include = ["slack*", "slack_sdk*"]
 [tool.setuptools.dynamic]
 version = { attr = "slack_sdk.version.__version__" }
 readme = { file = ["README.md"], content-type = "text/markdown" }
+optional-dependencies.optional = { file = ["requirements/optional.txt"] }
 
 [tool.distutils.bdist_wheel]
 universal = true
diff --git a/slack_sdk/models/blocks/basic_components.py b/slack_sdk/models/blocks/basic_components.py
index 3999b91..dc23736 100644
--- a/slack_sdk/models/blocks/basic_components.py
+++ b/slack_sdk/models/blocks/basic_components.py
@@ -559,3 +559,32 @@ class Workflow(JsonObject):
         else:
             json["trigger"] = self._trigger
         return json
+
+
+class SlackFile(JsonObject):
+    attributes = {"id", "url"}
+
+    def __init__(
+        self,
+        *,
+        id: Optional[str] = None,
+        url: Optional[str] = None,
+    ):
+        """An object containing Slack file information to be used in an image block or image element.
+        https://api.slack.com/reference/block-kit/composition-objects#slack_file
+
+        Args:
+            id: Slack ID of the file.
+            url: This URL can be the url_private or the permalink of the Slack file.
+        """
+        self._id = id
+        self._url = url
+
+    def to_dict(self) -> Dict[str, Any]:  # skipcq: PYL-W0221
+        self.validate_json()
+        json = {}
+        if self._id is not None:
+            json["id"] = self._id
+        if self._url is not None:
+            json["url"] = self._url
+        return json
diff --git a/slack_sdk/models/blocks/block_elements.py b/slack_sdk/models/blocks/block_elements.py
index 52af63b..8977001 100644
--- a/slack_sdk/models/blocks/block_elements.py
+++ b/slack_sdk/models/blocks/block_elements.py
@@ -11,7 +11,7 @@ from slack_sdk.models.basic_objects import (
     JsonValidator,
     EnumValidator,
 )
-from .basic_components import ButtonStyles, Workflow
+from .basic_components import ButtonStyles, Workflow, SlackFile
 from .basic_components import ConfirmObject
 from .basic_components import DispatchActionConfig
 from .basic_components import MarkdownTextObject
@@ -551,13 +551,14 @@ class ImageElement(BlockElement):
 
     @property
     def attributes(self) -> Set[str]:
-        return super().attributes.union({"alt_text", "image_url"})
+        return super().attributes.union({"alt_text", "image_url", "slack_file"})
 
     def __init__(
         self,
         *,
-        image_url: Optional[str] = None,
         alt_text: Optional[str] = None,
+        image_url: Optional[str] = None,
+        slack_file: Optional[Union[Dict[str, Any], SlackFile]] = None,
         **others: dict,
     ):
         """An element to insert an image - this element can be used in section and
@@ -566,18 +567,20 @@ class ImageElement(BlockElement):
         https://api.slack.com/reference/block-kit/block-elements#image
 
         Args:
-            image_url (required): The URL of the image to be displayed.
             alt_text (required): A plain-text summary of the image. This should not contain any markup.
+            image_url: The URL of the image to be displayed.
+            slack_file: A Slack image file object that defines the source of the image.
         """
         super().__init__(type=self.type)
         show_unknown_key_warning(self, others)
 
         self.image_url = image_url
         self.alt_text = alt_text
+        self.slack_file = slack_file if slack_file is None or isinstance(slack_file, SlackFile) else SlackFile(**slack_file)
 
     @JsonValidator(f"image_url attribute cannot exceed {image_url_max_length} characters")
     def _validate_image_url_length(self) -> bool:
-        return len(self.image_url) <= self.image_url_max_length
+        return self.image_url is None or len(self.image_url) <= self.image_url_max_length
 
     @JsonValidator(f"alt_text attribute cannot exceed {alt_text_max_length} characters")
     def _validate_alt_text_length(self) -> bool:
diff --git a/slack_sdk/models/blocks/blocks.py b/slack_sdk/models/blocks/blocks.py
index 5d67213..2e9fa17 100644
--- a/slack_sdk/models/blocks/blocks.py
+++ b/slack_sdk/models/blocks/blocks.py
@@ -8,7 +8,7 @@ from slack_sdk.models.basic_objects import (
     JsonObject,
     JsonValidator,
 )
-from .basic_components import MarkdownTextObject
+from .basic_components import MarkdownTextObject, SlackFile
 from .basic_components import PlainTextObject
 from .basic_components import TextObject
 from .block_elements import BlockElement, RichTextElement
@@ -208,7 +208,7 @@ class ImageBlock(Block):
 
     @property
     def attributes(self) -> Set[str]:
-        return super().attributes.union({"alt_text", "image_url", "title"})
+        return super().attributes.union({"alt_text", "image_url", "title", "slack_file"})
 
     image_url_max_length = 3000
     alt_text_max_length = 2000
@@ -217,8 +217,9 @@ class ImageBlock(Block):
     def __init__(
         self,
         *,
-        image_url: str,
         alt_text: str,
+        image_url: Optional[str] = None,
+        slack_file: Optional[Union[Dict[str, Any], SlackFile]] = None,
         title: Optional[Union[str, dict, PlainTextObject]] = None,
         block_id: Optional[str] = None,
         **others: dict,
@@ -227,10 +228,11 @@ class ImageBlock(Block):
         https://api.slack.com/reference/block-kit/blocks#image
 
         Args:
-            image_url (required): The URL of the image to be displayed.
-                Maximum length for this field is 3000 characters.
             alt_text (required): A plain-text summary of the image. This should not contain any markup.
                 Maximum length for this field is 2000 characters.
+            image_url: The URL of the image to be displayed.
+                Maximum length for this field is 3000 characters.
+            slack_file: A Slack image file object that defines the source of the image.
             title: An optional title for the image in the form of a text object that can only be of type: plain_text.
                 Maximum length for the text in this field is 2000 characters.
             block_id: A string acting as a unique identifier for a block. If not specified, one will be generated.
@@ -255,11 +257,15 @@ class ImageBlock(Block):
                 parsed_title = title
             else:
                 raise SlackObjectFormationError(f"Unsupported type for title in an image block: {type(title)}")
+        if slack_file is not None:
+            self.slack_file = (
+                slack_file if slack_file is None or isinstance(slack_file, SlackFile) else SlackFile(**slack_file)
+            )
         self.title = parsed_title
 
     @JsonValidator(f"image_url attribute cannot exceed {image_url_max_length} characters")
     def _validate_image_url_length(self):
-        return len(self.image_url) <= self.image_url_max_length
+        return self.image_url is None or len(self.image_url) <= self.image_url_max_length
 
     @JsonValidator(f"alt_text attribute cannot exceed {alt_text_max_length} characters")
     def _validate_alt_text_length(self):


Test patch:
diff --git a/tests/slack_sdk/models/test_blocks.py b/tests/slack_sdk/models/test_blocks.py
index 836f65b..92abc76 100644
--- a/tests/slack_sdk/models/test_blocks.py
+++ b/tests/slack_sdk/models/test_blocks.py
@@ -29,6 +29,7 @@ from slack_sdk.models.blocks import (
     RichTextPreformattedElement,
     RichTextElementParts,
 )
+from slack_sdk.models.blocks.basic_components import SlackFile
 from . import STRING_3001_CHARS
 
 
@@ -151,7 +152,7 @@ class SectionBlockTests(unittest.TestCase):
             SectionBlock(text="some text", fields=[f"field{i}" for i in range(5)]).to_dict(),
         )
 
-        button = LinkButtonElement(text="Click me!", url="http://google.com")
+        button = LinkButtonElement(text="Click me!", url="https://example.com")
         self.assertDictEqual(
             {
                 "type": "section",
@@ -314,11 +315,11 @@ class ImageBlockTests(unittest.TestCase):
     def test_json(self):
         self.assertDictEqual(
             {
-                "image_url": "http://google.com",
+                "image_url": "https://example.com",
                 "alt_text": "not really an image",
                 "type": "image",
             },
-            ImageBlock(image_url="http://google.com", alt_text="not really an image").to_dict(),
+            ImageBlock(image_url="https://example.com", alt_text="not really an image").to_dict(),
         )
 
     def test_image_url_length(self):
@@ -327,11 +328,45 @@ class ImageBlockTests(unittest.TestCase):
 
     def test_alt_text_length(self):
         with self.assertRaises(SlackObjectFormationError):
-            ImageBlock(image_url="http://google.com", alt_text=STRING_3001_CHARS).to_dict()
+            ImageBlock(image_url="https://example.com", alt_text=STRING_3001_CHARS).to_dict()
 
     def test_title_length(self):
         with self.assertRaises(SlackObjectFormationError):
-            ImageBlock(image_url="http://google.com", alt_text="text", title=STRING_3001_CHARS).to_dict()
+            ImageBlock(image_url="https://example.com", alt_text="text", title=STRING_3001_CHARS).to_dict()
+
+    def test_slack_file(self):
+        self.assertDictEqual(
+            {
+                "slack_file": {"url": "https://example.com"},
+                "alt_text": "not really an image",
+                "type": "image",
+            },
+            ImageBlock(slack_file=SlackFile(url="https://example.com"), alt_text="not really an image").to_dict(),
+        )
+        self.assertDictEqual(
+            {
+                "slack_file": {"id": "F11111"},
+                "alt_text": "not really an image",
+                "type": "image",
+            },
+            ImageBlock(slack_file=SlackFile(id="F11111"), alt_text="not really an image").to_dict(),
+        )
+        self.assertDictEqual(
+            {
+                "slack_file": {"url": "https://example.com"},
+                "alt_text": "not really an image",
+                "type": "image",
+            },
+            ImageBlock(slack_file={"url": "https://example.com"}, alt_text="not really an image").to_dict(),
+        )
+        self.assertDictEqual(
+            {
+                "slack_file": {"id": "F11111"},
+                "alt_text": "not really an image",
+                "type": "image",
+            },
+            ImageBlock(slack_file={"id": "F11111"}, alt_text="not really an image").to_dict(),
+        )
 
 
 # ----------------------------------------------
@@ -446,7 +481,7 @@ class ActionsBlockTests(unittest.TestCase):
     def test_json(self):
         self.elements = [
             ButtonElement(text="Click me", action_id="reg_button", value="1"),
-            LinkButtonElement(text="URL Button", url="http://google.com"),
+            LinkButtonElement(text="URL Button", url="https://example.com"),
         ]
         self.dict_elements = []
         for e in self.elements:
diff --git a/tests/slack_sdk/models/test_elements.py b/tests/slack_sdk/models/test_elements.py
index 5a38e14..8fa7bd0 100644
--- a/tests/slack_sdk/models/test_elements.py
+++ b/tests/slack_sdk/models/test_elements.py
@@ -28,6 +28,7 @@ from slack_sdk.models.blocks import (
     InteractiveElement,
     PlainTextObject,
 )
+from slack_sdk.models.blocks.basic_components import SlackFile
 from slack_sdk.models.blocks.block_elements import (
     DateTimePickerElement,
     EmailInputElement,
@@ -186,11 +187,11 @@ class ButtonElementTests(unittest.TestCase):
 
 class LinkButtonElementTests(unittest.TestCase):
     def test_json(self):
-        button = LinkButtonElement(action_id="test", text="button text", url="http://google.com")
+        button = LinkButtonElement(action_id="test", text="button text", url="https://example.com")
         self.assertDictEqual(
             {
                 "text": {"emoji": True, "text": "button text", "type": "plain_text"},
-                "url": "http://google.com",
+                "url": "https://example.com",
                 "type": "button",
                 "action_id": button.action_id,
             },
@@ -456,11 +457,11 @@ class ImageElementTests(unittest.TestCase):
     def test_json(self):
         self.assertDictEqual(
             {
-                "image_url": "http://google.com",
+                "image_url": "https://example.com",
                 "alt_text": "not really an image",
                 "type": "image",
             },
-            ImageElement(image_url="http://google.com", alt_text="not really an image").to_dict(),
+            ImageElement(image_url="https://example.com", alt_text="not really an image").to_dict(),
         )
 
     def test_image_url_length(self):
@@ -469,7 +470,41 @@ class ImageElementTests(unittest.TestCase):
 
     def test_alt_text_length(self):
         with self.assertRaises(SlackObjectFormationError):
-            ImageElement(image_url="http://google.com", alt_text=STRING_3001_CHARS).to_dict()
+            ImageElement(image_url="https://example.com", alt_text=STRING_3001_CHARS).to_dict()
+
+    def test_slack_file(self):
+        self.assertDictEqual(
+            {
+                "slack_file": {"id": "F11111"},
+                "alt_text": "not really an image",
+                "type": "image",
+            },
+            ImageElement(slack_file=SlackFile(id="F11111"), alt_text="not really an image").to_dict(),
+        )
+        self.assertDictEqual(
+            {
+                "slack_file": {"url": "https://example.com"},
+                "alt_text": "not really an image",
+                "type": "image",
+            },
+            ImageElement(slack_file=SlackFile(url="https://example.com"), alt_text="not really an image").to_dict(),
+        )
+        self.assertDictEqual(
+            {
+                "slack_file": {"id": "F11111"},
+                "alt_text": "not really an image",
+                "type": "image",
+            },
+            ImageElement(slack_file={"id": "F11111"}, alt_text="not really an image").to_dict(),
+        )
+        self.assertDictEqual(
+            {
+                "slack_file": {"url": "https://example.com"},
+                "alt_text": "not really an image",
+                "type": "image",
+            },
+            ImageElement(slack_file={"url": "https://example.com"}, alt_text="not really an image").to_dict(),
+        )
 
 
 # -------------------------------------------------