From 1244019c77c17da56526cce0381bbc9063bda78c Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Mon, 1 Dec 2025 15:20:53 -0500
Subject: [PATCH 01/13] chore(deps): bump actions/checkout from 5.0.0 to 6.0.0
(#1799)
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
.github/workflows/pypi-release.yml | 2 +-
.github/workflows/tests.yml | 4 ++--
2 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/.github/workflows/pypi-release.yml b/.github/workflows/pypi-release.yml
index 21b472247..24ae2eb75 100644
--- a/.github/workflows/pypi-release.yml
+++ b/.github/workflows/pypi-release.yml
@@ -18,7 +18,7 @@ jobs:
contents: read
steps:
- - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
+ - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
with:
ref: ${{ github.event.release.tag_name || github.ref }}
persist-credentials: false
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index 92ed007c8..18db5d80d 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -21,7 +21,7 @@ jobs:
permissions:
contents: read
steps:
- - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
+ - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
with:
persist-credentials: false
- name: Set up Python ${{ matrix.python-version }}
@@ -54,7 +54,7 @@ jobs:
CI_LARGE_SOCKET_MODE_PAYLOAD_TESTING_DISABLED: "1"
FORCE_COLOR: "1"
steps:
- - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
+ - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
with:
persist-credentials: false
- name: Set up Python ${{ matrix.python-version }}
From fe1f38eea302a0a7716763fee136b82f0bf86ab9 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Mon, 1 Dec 2025 15:33:27 -0500
Subject: [PATCH 02/13] chore(deps): bump actions/setup-python from 6.0.0 to
6.1.0 (#1800)
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: William Bergamin
---
.github/workflows/pypi-release.yml | 2 +-
.github/workflows/tests.yml | 4 ++--
2 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/.github/workflows/pypi-release.yml b/.github/workflows/pypi-release.yml
index 24ae2eb75..80fac6d8c 100644
--- a/.github/workflows/pypi-release.yml
+++ b/.github/workflows/pypi-release.yml
@@ -24,7 +24,7 @@ jobs:
persist-credentials: false
- name: Set up Python
- uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
+ uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
with:
python-version: "3.x"
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index 18db5d80d..ac34e26fd 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -25,7 +25,7 @@ jobs:
with:
persist-credentials: false
- name: Set up Python ${{ matrix.python-version }}
- uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
+ uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
with:
python-version: ${{ matrix.python-version }}
- name: Run mypy verification
@@ -58,7 +58,7 @@ jobs:
with:
persist-credentials: false
- name: Set up Python ${{ matrix.python-version }}
- uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
+ uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
with:
python-version: ${{ matrix.python-version }}
cache: pip
From 9b276eb4b64624397e52c0d7df12dc6798778531 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Mon, 1 Dec 2025 16:03:18 -0500
Subject: [PATCH 03/13] chore(deps): bump docutils from 0.22.2 to 0.22.3
(#1802)
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
requirements/documentation.txt | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/requirements/documentation.txt b/requirements/documentation.txt
index af8c47639..26d7aaa41 100644
--- a/requirements/documentation.txt
+++ b/requirements/documentation.txt
@@ -1,2 +1,2 @@
-docutils==0.22.2
+docutils==0.22.3
pdoc3==0.11.6
From 4601f8a82836cf649ffe0f5d7b08061882d6361f Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Tue, 2 Dec 2025 11:19:24 -0500
Subject: [PATCH 04/13] chore(deps): update mypy requirement from <=1.18.2 to
<=1.19.0 (#1801)
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Michael Brooks
Co-authored-by: William Bergamin
---
requirements/tools.txt | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/requirements/tools.txt b/requirements/tools.txt
index 0a646dc12..dd09cd46f 100644
--- a/requirements/tools.txt
+++ b/requirements/tools.txt
@@ -1,4 +1,6 @@
-mypy<=1.18.2
+# We only need to install mypy with CPython runtimes
+# Typechecking using the PyPy runtime is not planned
+mypy<=1.19.0; platform_python_implementation == "CPython"
# while flake8 5.x have issues with Python 3.12, flake8 6.x requires Python >= 3.8.1,
# so 5.x should be kept in order to stay compatible with Python 3.7/3.8
flake8>=5.0.4,<8
From dfa5e5f01020df1782ffd2ed65a8a781218fc664 Mon Sep 17 00:00:00 2001
From: William Bergamin
Date: Fri, 5 Dec 2025 15:38:18 -0500
Subject: [PATCH 05/13] fix: improve CI and helper scripts (#1803)
Co-authored-by: Eden Zimbelman
---
.github/workflows/{tests.yml => ci-build.yml} | 45 +++++++++++++------
requirements/testing.txt | 1 -
requirements/tools.txt | 4 +-
scripts/format.sh | 7 ++-
scripts/lint.sh | 14 ++++++
scripts/run_integration_tests.sh | 5 ++-
scripts/run_mypy.sh | 3 +-
scripts/run_unit_tests.sh | 6 ++-
scripts/run_validation.sh | 9 ++--
9 files changed, 65 insertions(+), 29 deletions(-)
rename .github/workflows/{tests.yml => ci-build.yml} (79%)
create mode 100755 scripts/lint.sh
diff --git a/.github/workflows/tests.yml b/.github/workflows/ci-build.yml
similarity index 79%
rename from .github/workflows/tests.yml
rename to .github/workflows/ci-build.yml
index ac34e26fd..7e9e4e28d 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/ci-build.yml
@@ -1,5 +1,4 @@
-# For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions
-name: Test
+name: Python CI
on:
push:
@@ -10,27 +9,44 @@ on:
- cron: "0 0 * * *"
workflow_dispatch:
+env:
+ LATEST_SUPPORTED_PY: "3.14"
+
jobs:
+ lint:
+ name: Lint
+ runs-on: ubuntu-latest
+ timeout-minutes: 5
+ permissions:
+ contents: read
+ steps:
+ - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
+ with:
+ persist-credentials: false
+ - name: Set up Python ${{ env.LATEST_SUPPORTED_PY }}
+ uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
+ with:
+ python-version: ${{ env.LATEST_SUPPORTED_PY }}
+ - name: Run lint verification
+ run: ./scripts/lint.sh
+
typecheck:
- name: Typechecks
+ name: Typecheck
runs-on: ubuntu-latest
timeout-minutes: 5
- strategy:
- matrix:
- python-version: ["3.14"]
permissions:
contents: read
steps:
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
with:
persist-credentials: false
- - name: Set up Python ${{ matrix.python-version }}
+ - name: Set up Python ${{ env.LATEST_SUPPORTED_PY }}
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
with:
- python-version: ${{ matrix.python-version }}
+ python-version: ${{ env.LATEST_SUPPORTED_PY }}
- name: Run mypy verification
- run: |
- ./scripts/run_mypy.sh
+ run: ./scripts/run_mypy.sh
+
unittest:
name: Unit tests
runs-on: ubuntu-22.04
@@ -48,6 +64,7 @@ jobs:
- "3.8"
- "3.7"
- "pypy3.10"
+ - "pypy3.11"
permissions:
contents: read
env:
@@ -67,10 +84,8 @@ jobs:
pip install -U pip
pip install -r requirements/testing.txt
pip install -r requirements/optional.txt
- - name: Run validation (black/flake8/pytest)
+ - name: Run tests
run: |
- black --check slack/ slack_sdk/ tests/ integration_tests/
- flake8 slack/ slack_sdk/
PYTHONPATH=$PWD:$PYTHONPATH pytest --cov-report=xml --cov=slack_sdk/ --junitxml=reports/test_report.xml tests/
- name: Run tests for SQLAlchemy v1.4 (backward-compatibility)
run: |
@@ -89,7 +104,7 @@ jobs:
token: ${{ secrets.CODECOV_TOKEN }}
verbose: true
- name: Upload test coverage to Codecov (only with latest supported version)
- if: startsWith(matrix.python-version, '3.14')
+ if: startsWith(matrix.python-version, env.LATEST_SUPPORTED_PY)
uses: codecov/codecov-action@5a1091511ad55cbe89839c7260b706298ca349f7 # v5.5.1
with:
fail_ci_if_error: true
@@ -98,10 +113,12 @@ jobs:
report_type: coverage
token: ${{ secrets.CODECOV_TOKEN }}
verbose: true
+
notifications:
name: Regression notifications
runs-on: ubuntu-latest
needs:
+ - lint
- typecheck
- unittest
if: ${{ !success() && github.ref == 'refs/heads/main' && github.event_name != 'workflow_dispatch' }}
diff --git a/requirements/testing.txt b/requirements/testing.txt
index 9b702718c..9e1a3fd67 100644
--- a/requirements/testing.txt
+++ b/requirements/testing.txt
@@ -12,4 +12,3 @@ moto>=4.0.13,<6
# For AsyncSQLAlchemy tests
greenlet<=4
aiosqlite<=1
--r tools.txt
diff --git a/requirements/tools.txt b/requirements/tools.txt
index dd09cd46f..4c0751dbd 100644
--- a/requirements/tools.txt
+++ b/requirements/tools.txt
@@ -1,6 +1,4 @@
-# We only need to install mypy with CPython runtimes
-# Typechecking using the PyPy runtime is not planned
-mypy<=1.19.0; platform_python_implementation == "CPython"
+mypy<=1.19.0;
# while flake8 5.x have issues with Python 3.12, flake8 6.x requires Python >= 3.8.1,
# so 5.x should be kept in order to stay compatible with Python 3.7/3.8
flake8>=5.0.4,<8
diff --git a/scripts/format.sh b/scripts/format.sh
index d4236258f..56ca68077 100755
--- a/scripts/format.sh
+++ b/scripts/format.sh
@@ -4,7 +4,10 @@
script_dir=`dirname $0`
cd ${script_dir}/..
-pip install -U pip
-pip install -U -r requirements/tools.txt
+if [[ "$1" != "--no-install" ]]; then
+ export PIP_REQUIRE_VIRTUALENV=1
+ pip install -U pip
+ pip install -U -r requirements/tools.txt
+fi
black slack/ slack_sdk/ tests/ integration_tests/
diff --git a/scripts/lint.sh b/scripts/lint.sh
new file mode 100755
index 000000000..8fac67888
--- /dev/null
+++ b/scripts/lint.sh
@@ -0,0 +1,14 @@
+
+#!/bin/bash
+# ./scripts/lint.sh
+
+script_dir=`dirname $0`
+cd ${script_dir}/..
+
+if [[ "$1" != "--no-install" ]]; then
+ pip install -U pip
+ pip install -U -r requirements/tools.txt
+fi
+
+black --check slack/ slack_sdk/ tests/ integration_tests/
+flake8 slack/ slack_sdk/
diff --git a/scripts/run_integration_tests.sh b/scripts/run_integration_tests.sh
index a5d86801b..1a6f254cb 100755
--- a/scripts/run_integration_tests.sh
+++ b/scripts/run_integration_tests.sh
@@ -10,10 +10,11 @@ cd ${script_dir}/..
pip install -U pip
pip install -U -r requirements/testing.txt \
- -U -r requirements/optional.txt
+ -U -r requirements/optional.txt \
+ -U -r requirements/tools.txt
echo "Generating code ..." && python scripts/codegen.py --path .
-echo "Running black (code formatter) ..." && black slack_sdk/
+echo "Running black (code formatter) ..." && ./scripts/format.sh --no-install
test_target="${1:-tests/integration_tests/}"
PYTHONPATH=$PWD:$PYTHONPATH pytest $test_target
diff --git a/scripts/run_mypy.sh b/scripts/run_mypy.sh
index 0d27346bc..cc1146f15 100755
--- a/scripts/run_mypy.sh
+++ b/scripts/run_mypy.sh
@@ -8,6 +8,7 @@ cd ${script_dir}/..
pip install -U pip setuptools wheel
pip install -U -r requirements/testing.txt \
- -U -r requirements/optional.txt
+ -U -r requirements/optional.txt \
+ -U -r requirements/tools.txt
mypy --config-file pyproject.toml
diff --git a/scripts/run_unit_tests.sh b/scripts/run_unit_tests.sh
index cec153388..c8ab0af78 100755
--- a/scripts/run_unit_tests.sh
+++ b/scripts/run_unit_tests.sh
@@ -10,10 +10,12 @@ cd ${script_dir}/..
pip install -U pip
pip install -U -r requirements/testing.txt \
- -U -r requirements/optional.txt
+ -U -r requirements/optional.txt \
+ -U -r requirements/tools.txt
echo "Generating code ..." && python scripts/codegen.py --path .
-echo "Running black (code formatter) ..." && black slack_sdk/
+echo "Running black (code formatter) ..." && ./scripts/format.sh --no-install
+echo "Running tests ..."
test_target="${1:-tests/}"
PYTHONPATH=$PWD:$PYTHONPATH pytest $test_target
diff --git a/scripts/run_validation.sh b/scripts/run_validation.sh
index 8a61d03a6..366f0d321 100755
--- a/scripts/run_validation.sh
+++ b/scripts/run_validation.sh
@@ -8,13 +8,14 @@ script_dir=`dirname $0`
cd ${script_dir}/..
pip install -U -r requirements/testing.txt \
- -U -r requirements/optional.txt
+ -U -r requirements/optional.txt \
+ -U -r requirements/tools.txt
echo "Generating code ..." && python scripts/codegen.py --path .
-echo "Running black (code formatter) ..." && black slack_sdk/
+echo "Running black (code formatter) ..." && ./scripts/format.sh --no-install
-black --check slack/ slack_sdk/ tests/ integration_tests/
-flake8 slack/ slack_sdk/
+echo "Running linting checks ..." && ./scripts/lint.sh --no-install
+echo "Running tests with coverage reporting ..."
test_target="${1:-tests/}"
PYTHONPATH=$PWD:$PYTHONPATH pytest --cov-report=xml --cov=slack_sdk/ $test_target
From b7eb2f973b4497b5d0e44ff5dcc52d40a51d7daa Mon Sep 17 00:00:00 2001
From: William Bergamin
Date: Fri, 5 Dec 2025 15:55:29 -0500
Subject: [PATCH 06/13] fix: move away from datetime.utcfromtimestamp for the
state and installation stores (#1798)
---
integration_tests/samples/oauth/oauth_v2.py | 10 ++++-
.../samples/oauth/oauth_v2_async.py | 8 +++-
.../oauth/installation_store/models/bot.py | 8 ++--
.../installation_store/models/installation.py | 12 ++++--
.../oauth/state_store/sqlalchemy/__init__.py | 10 ++---
.../oauth/installation_store/test_models.py | 43 +++++++++++++++++++
6 files changed, 76 insertions(+), 15 deletions(-)
diff --git a/integration_tests/samples/oauth/oauth_v2.py b/integration_tests/samples/oauth/oauth_v2.py
index 5741b4ef8..c1c32d879 100644
--- a/integration_tests/samples/oauth/oauth_v2.py
+++ b/integration_tests/samples/oauth/oauth_v2.py
@@ -123,13 +123,19 @@ def oauth_callback():
@app.route("/slack/events", methods=["POST"])
def slack_app():
+ data = request.get_data()
if not signature_verifier.is_valid(
- body=request.get_data(),
+ body=data,
timestamp=request.headers.get("X-Slack-Request-Timestamp"),
signature=request.headers.get("X-Slack-Signature"),
):
return make_response("invalid request", 403)
+ if data and b"url_verification" in data:
+ body = json.loads(data)
+ if body.get("type") == "url_verification" and "challenge" in body:
+ return make_response(body["challenge"], 200)
+
if "command" in request.form and request.form["command"] == "/open-modal":
try:
enterprise_id = request.form.get("enterprise_id")
@@ -193,4 +199,4 @@ def slack_app():
# python3 integration_tests/samples/oauth/oauth_v2.py
# ngrok http 3000
- # https://{yours}.ngrok.io/slack/oauth/start
+ # https://{yours}.ngrok.io/slack/install
diff --git a/integration_tests/samples/oauth/oauth_v2_async.py b/integration_tests/samples/oauth/oauth_v2_async.py
index c43add6f3..588b0b5f0 100644
--- a/integration_tests/samples/oauth/oauth_v2_async.py
+++ b/integration_tests/samples/oauth/oauth_v2_async.py
@@ -135,13 +135,19 @@ async def oauth_callback(req: Request):
@app.post("/slack/events")
async def slack_app(req: Request):
+ data = req.body.decode("utf-8")
if not signature_verifier.is_valid(
- body=req.body.decode("utf-8"),
+ body=data,
timestamp=req.headers.get("X-Slack-Request-Timestamp"),
signature=req.headers.get("X-Slack-Signature"),
):
return HTTPResponse(status=403, body="invalid request")
+ if data and "url_verification" in data:
+ body = json.loads(data)
+ if body.get("type") == "url_verification" and "challenge" in body:
+ return HTTPResponse(status=200, body=body["challenge"])
+
if "command" in req.form and req.form.get("command") == "/open-modal":
try:
enterprise_id = req.form.get("enterprise_id")
diff --git a/slack_sdk/oauth/installation_store/models/bot.py b/slack_sdk/oauth/installation_store/models/bot.py
index 52c1dac50..3f2f6de81 100644
--- a/slack_sdk/oauth/installation_store/models/bot.py
+++ b/slack_sdk/oauth/installation_store/models/bot.py
@@ -1,4 +1,4 @@
-from datetime import datetime
+from datetime import datetime, timezone
from time import time
from typing import Optional, Union, Dict, Any, Sequence
@@ -100,10 +100,12 @@ def _to_standard_value_dict(self) -> Dict[str, Any]:
"bot_scopes": ",".join(self.bot_scopes) if self.bot_scopes else None,
"bot_refresh_token": self.bot_refresh_token,
"bot_token_expires_at": (
- datetime.utcfromtimestamp(self.bot_token_expires_at) if self.bot_token_expires_at is not None else None
+ datetime.fromtimestamp(self.bot_token_expires_at, tz=timezone.utc)
+ if self.bot_token_expires_at is not None
+ else None
),
"is_enterprise_install": self.is_enterprise_install,
- "installed_at": datetime.utcfromtimestamp(self.installed_at),
+ "installed_at": datetime.fromtimestamp(self.installed_at, tz=timezone.utc),
}
def to_dict_for_copying(self) -> Dict[str, Any]:
diff --git a/slack_sdk/oauth/installation_store/models/installation.py b/slack_sdk/oauth/installation_store/models/installation.py
index 91c6510f2..18ca8e0b1 100644
--- a/slack_sdk/oauth/installation_store/models/installation.py
+++ b/slack_sdk/oauth/installation_store/models/installation.py
@@ -1,4 +1,4 @@
-from datetime import datetime
+from datetime import datetime, timezone
from time import time
from typing import Optional, Union, Dict, Any, Sequence
@@ -173,14 +173,18 @@ def _to_standard_value_dict(self) -> Dict[str, Any]:
"bot_scopes": ",".join(self.bot_scopes) if self.bot_scopes else None,
"bot_refresh_token": self.bot_refresh_token,
"bot_token_expires_at": (
- datetime.utcfromtimestamp(self.bot_token_expires_at) if self.bot_token_expires_at is not None else None
+ datetime.fromtimestamp(self.bot_token_expires_at, tz=timezone.utc)
+ if self.bot_token_expires_at is not None
+ else None
),
"user_id": self.user_id,
"user_token": self.user_token,
"user_scopes": ",".join(self.user_scopes) if self.user_scopes else None,
"user_refresh_token": self.user_refresh_token,
"user_token_expires_at": (
- datetime.utcfromtimestamp(self.user_token_expires_at) if self.user_token_expires_at is not None else None
+ datetime.fromtimestamp(self.user_token_expires_at, tz=timezone.utc)
+ if self.user_token_expires_at is not None
+ else None
),
"incoming_webhook_url": self.incoming_webhook_url,
"incoming_webhook_channel": self.incoming_webhook_channel,
@@ -188,7 +192,7 @@ def _to_standard_value_dict(self) -> Dict[str, Any]:
"incoming_webhook_configuration_url": self.incoming_webhook_configuration_url,
"is_enterprise_install": self.is_enterprise_install,
"token_type": self.token_type,
- "installed_at": datetime.utcfromtimestamp(self.installed_at),
+ "installed_at": datetime.fromtimestamp(self.installed_at, tz=timezone.utc),
}
def to_dict_for_copying(self) -> Dict[str, Any]:
diff --git a/slack_sdk/oauth/state_store/sqlalchemy/__init__.py b/slack_sdk/oauth/state_store/sqlalchemy/__init__.py
index b26f642cd..8bb3ec1ff 100644
--- a/slack_sdk/oauth/state_store/sqlalchemy/__init__.py
+++ b/slack_sdk/oauth/state_store/sqlalchemy/__init__.py
@@ -1,6 +1,6 @@
import logging
import time
-from datetime import datetime
+from datetime import datetime, timezone
from logging import Logger
from uuid import uuid4
@@ -55,7 +55,7 @@ def logger(self) -> Logger:
def issue(self, *args, **kwargs) -> str:
state: str = str(uuid4())
- now = datetime.utcfromtimestamp(time.time() + self.expiration_seconds)
+ now = datetime.fromtimestamp(time.time() + self.expiration_seconds, tz=timezone.utc)
with self.engine.begin() as conn:
conn.execute(
self.oauth_states.insert(),
@@ -67,7 +67,7 @@ def consume(self, state: str) -> bool:
try:
with self.engine.begin() as conn:
c = self.oauth_states.c
- query = self.oauth_states.select().where(and_(c.state == state, c.expire_at > datetime.utcnow()))
+ query = self.oauth_states.select().where(and_(c.state == state, c.expire_at > datetime.now(tz=timezone.utc)))
result = conn.execute(query)
for row in result.mappings():
self.logger.debug(f"consume's query result: {row}")
@@ -124,7 +124,7 @@ def logger(self) -> Logger:
async def async_issue(self, *args, **kwargs) -> str:
state: str = str(uuid4())
- now = datetime.utcfromtimestamp(time.time() + self.expiration_seconds)
+ now = datetime.fromtimestamp(time.time() + self.expiration_seconds, tz=timezone.utc)
async with self.engine.begin() as conn:
await conn.execute(
self.oauth_states.insert(),
@@ -136,7 +136,7 @@ async def async_consume(self, state: str) -> bool:
try:
async with self.engine.begin() as conn:
c = self.oauth_states.c
- query = self.oauth_states.select().where(and_(c.state == state, c.expire_at > datetime.utcnow()))
+ query = self.oauth_states.select().where(and_(c.state == state, c.expire_at > datetime.now(tz=timezone.utc)))
result = await conn.execute(query)
for row in result.mappings():
self.logger.debug(f"consume's query result: {row}")
diff --git a/tests/slack_sdk/oauth/installation_store/test_models.py b/tests/slack_sdk/oauth/installation_store/test_models.py
index d63964be6..43b1ec7b2 100644
--- a/tests/slack_sdk/oauth/installation_store/test_models.py
+++ b/tests/slack_sdk/oauth/installation_store/test_models.py
@@ -1,4 +1,5 @@
import time
+from datetime import datetime, timezone
import unittest
from slack_sdk.oauth.installation_store import Installation, FileInstallationStore, Bot
@@ -36,6 +37,22 @@ def test_bot_custom_fields(self):
self.assertEqual(bot.to_dict().get("service_user_id"), "XYZ123")
self.assertEqual(bot.to_dict_for_copying().get("custom_values").get("service_user_id"), "XYZ123")
+ def test_bot_datetime_manipulation(self):
+ expected_timestamp = datetime.now(tz=timezone.utc)
+ bot = Bot(
+ bot_token="xoxb-",
+ bot_id="B111",
+ bot_user_id="U111",
+ bot_token_expires_at=expected_timestamp,
+ installed_at=expected_timestamp,
+ )
+ bot_dict = bot.to_dict()
+ self.assertIsNotNone(bot_dict)
+ self.assertEqual(
+ bot_dict.get("bot_token_expires_at").isoformat(), expected_timestamp.strftime("%Y-%m-%dT%H:%M:%S+00:00")
+ )
+ self.assertEqual(bot_dict.get("installed_at"), expected_timestamp)
+
def test_installation(self):
installation = Installation(
app_id="A111",
@@ -84,3 +101,29 @@ def test_installation_custom_fields(self):
self.assertEqual(bot.to_dict().get("app_id"), "A111")
self.assertEqual(bot.to_dict().get("service_user_id"), "XYZ123")
self.assertEqual(bot.to_dict_for_copying().get("custom_values").get("app_id"), "A222")
+
+ def test_installation_datetime_manipulation(self):
+ expected_timestamp = datetime.now(tz=timezone.utc)
+ installation = Installation(
+ app_id="A111",
+ enterprise_id="E111",
+ team_id="T111",
+ user_id="U111",
+ bot_id="B111",
+ bot_token="xoxb-111",
+ bot_scopes=["chat:write"],
+ bot_user_id="U222",
+ bot_token_expires_at=expected_timestamp,
+ user_token_expires_at=expected_timestamp,
+ installed_at=expected_timestamp,
+ )
+ installation_dict = installation.to_dict()
+ self.assertIsNotNone(installation_dict)
+ self.assertEqual(
+ installation_dict.get("bot_token_expires_at").isoformat(), expected_timestamp.strftime("%Y-%m-%dT%H:%M:%S+00:00")
+ )
+ self.assertEqual(
+ installation_dict.get("user_token_expires_at").isoformat(),
+ expected_timestamp.strftime("%Y-%m-%dT%H:%M:%S+00:00"),
+ )
+ self.assertEqual(installation_dict.get("installed_at"), expected_timestamp)
From 96c0f840029f44c071f6a233b37c5d9e477d0ff7 Mon Sep 17 00:00:00 2001
From: William Bergamin
Date: Tue, 9 Dec 2025 15:29:39 -0500
Subject: [PATCH 07/13] fix: broken link to CI from readme (#1805)
---
README.md | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/README.md b/README.md
index 568efbea2..2d0638e7d 100644
--- a/README.md
+++ b/README.md
@@ -1,8 +1,8 @@
Python Slack SDK
-
-
+
+
From 1c84f7f77472700badaed9e1c880098fcc0ab81b Mon Sep 17 00:00:00 2001
From: Eden Zimbelman
Date: Fri, 16 Jan 2026 15:04:48 -0800
Subject: [PATCH 08/13] feat: accept chunks as arguments to
chat.{start,append,stop}Stream methods (#1806)
---
slack_sdk/models/messages/chunk.py | 155 +++++++++++++++++++++
slack_sdk/web/async_client.py | 11 +-
slack_sdk/web/client.py | 11 +-
slack_sdk/web/internal_utils.py | 14 +-
slack_sdk/web/legacy_client.py | 11 +-
tests/slack_sdk/models/test_chunks.py | 72 ++++++++++
tests/slack_sdk/web/test_internal_utils.py | 31 ++++-
7 files changed, 295 insertions(+), 10 deletions(-)
create mode 100644 slack_sdk/models/messages/chunk.py
create mode 100644 tests/slack_sdk/models/test_chunks.py
diff --git a/slack_sdk/models/messages/chunk.py b/slack_sdk/models/messages/chunk.py
new file mode 100644
index 000000000..837714af0
--- /dev/null
+++ b/slack_sdk/models/messages/chunk.py
@@ -0,0 +1,155 @@
+import logging
+from typing import Any, Dict, Optional, Sequence, Set, Union
+
+from slack_sdk.errors import SlackObjectFormationError
+from slack_sdk.models import show_unknown_key_warning
+from slack_sdk.models.basic_objects import JsonObject
+
+
+class Chunk(JsonObject):
+ """
+ Chunk for streaming messages.
+
+ https://docs.slack.dev/messaging/sending-and-scheduling-messages#text-streaming
+ """
+
+ attributes = {"type"}
+ logger = logging.getLogger(__name__)
+
+ def __init__(
+ self,
+ *,
+ type: Optional[str] = None,
+ ):
+ self.type = type
+
+ @classmethod
+ def parse(cls, chunk: Union[Dict, "Chunk"]) -> Optional["Chunk"]:
+ if chunk is None:
+ return None
+ elif isinstance(chunk, Chunk):
+ return chunk
+ else:
+ if "type" in chunk:
+ type = chunk["type"]
+ if type == MarkdownTextChunk.type:
+ return MarkdownTextChunk(**chunk)
+ elif type == TaskUpdateChunk.type:
+ return TaskUpdateChunk(**chunk)
+ else:
+ cls.logger.warning(f"Unknown chunk detected and skipped ({chunk})")
+ return None
+ else:
+ cls.logger.warning(f"Unknown chunk detected and skipped ({chunk})")
+ return None
+
+
+class MarkdownTextChunk(Chunk):
+ type = "markdown_text"
+
+ @property
+ def attributes(self) -> Set[str]: # type: ignore[override]
+ return super().attributes.union({"text"})
+
+ def __init__(
+ self,
+ *,
+ text: str,
+ **others: Dict,
+ ):
+ """Used for streaming text content with markdown formatting support.
+
+ https://docs.slack.dev/messaging/sending-and-scheduling-messages#text-streaming
+ """
+ super().__init__(type=self.type)
+ show_unknown_key_warning(self, others)
+
+ self.text = text
+
+
+class URLSource(JsonObject):
+ type = "url"
+
+ @property
+ def attributes(self) -> Set[str]:
+ return super().attributes.union(
+ {
+ "url",
+ "text",
+ "icon_url",
+ }
+ )
+
+ def __init__(
+ self,
+ *,
+ url: str,
+ text: str,
+ icon_url: Optional[str] = None,
+ **others: Dict,
+ ):
+ show_unknown_key_warning(self, others)
+ self._url = url
+ self._text = text
+ self._icon_url = icon_url
+
+ def to_dict(self) -> Dict[str, Any]:
+ self.validate_json()
+ json: Dict[str, Union[str, Dict]] = {
+ "type": self.type,
+ "url": self._url,
+ "text": self._text,
+ }
+ if self._icon_url:
+ json["icon_url"] = self._icon_url
+ return json
+
+
+class TaskUpdateChunk(Chunk):
+ type = "task_update"
+
+ @property
+ def attributes(self) -> Set[str]: # type: ignore[override]
+ return super().attributes.union(
+ {
+ "id",
+ "title",
+ "status",
+ "details",
+ "output",
+ "sources",
+ }
+ )
+
+ def __init__(
+ self,
+ *,
+ id: str,
+ title: str,
+ status: str, # "pending", "in_progress", "complete", "error"
+ details: Optional[str] = None,
+ output: Optional[str] = None,
+ sources: Optional[Sequence[Union[Dict, URLSource]]] = None,
+ **others: Dict,
+ ):
+ """Used for displaying tool execution progress in a timeline-style UI.
+
+ https://docs.slack.dev/messaging/sending-and-scheduling-messages#text-streaming
+ """
+ super().__init__(type=self.type)
+ show_unknown_key_warning(self, others)
+
+ self.id = id
+ self.title = title
+ self.status = status
+ self.details = details
+ self.output = output
+ if sources is not None:
+ self.sources = []
+ for src in sources:
+ if isinstance(src, Dict):
+ self.sources.append(src)
+ elif isinstance(src, URLSource):
+ self.sources.append(src.to_dict())
+ else:
+ raise SlackObjectFormationError(f"Unsupported type for source in task update chunk: {type(src)}")
diff --git a/slack_sdk/web/async_client.py b/slack_sdk/web/async_client.py
index ca163da98..0a9f702b9 100644
--- a/slack_sdk/web/async_client.py
+++ b/slack_sdk/web/async_client.py
@@ -17,12 +17,13 @@
from typing import Any, Dict, List, Optional, Sequence, Union
import slack_sdk.errors as e
+from slack_sdk.models.messages.chunk import Chunk
from slack_sdk.models.views import View
from slack_sdk.web.async_chat_stream import AsyncChatStream
from ..models.attachments import Attachment
from ..models.blocks import Block, RichTextBlock
-from ..models.metadata import Metadata, EntityMetadata, EventAndEntityMetadata
+from ..models.metadata import EntityMetadata, EventAndEntityMetadata, Metadata
from .async_base_client import AsyncBaseClient, AsyncSlackResponse
from .internal_utils import (
_parse_web_class_objects,
@@ -2631,6 +2632,7 @@ async def chat_appendStream(
channel: str,
ts: str,
markdown_text: str,
+ chunks: Optional[Sequence[Union[Dict, Chunk]]] = None,
**kwargs,
) -> AsyncSlackResponse:
"""Appends text to an existing streaming conversation.
@@ -2641,8 +2643,10 @@ async def chat_appendStream(
"channel": channel,
"ts": ts,
"markdown_text": markdown_text,
+ "chunks": chunks,
}
)
+ _parse_web_class_objects(kwargs)
kwargs = _remove_none_values(kwargs)
return await self.api_call("chat.appendStream", json=kwargs)
@@ -2884,6 +2888,7 @@ async def chat_startStream(
markdown_text: Optional[str] = None,
recipient_team_id: Optional[str] = None,
recipient_user_id: Optional[str] = None,
+ chunks: Optional[Sequence[Union[Dict, Chunk]]] = None,
**kwargs,
) -> AsyncSlackResponse:
"""Starts a new streaming conversation.
@@ -2896,8 +2901,10 @@ async def chat_startStream(
"markdown_text": markdown_text,
"recipient_team_id": recipient_team_id,
"recipient_user_id": recipient_user_id,
+ "chunks": chunks,
}
)
+ _parse_web_class_objects(kwargs)
kwargs = _remove_none_values(kwargs)
return await self.api_call("chat.startStream", json=kwargs)
@@ -2909,6 +2916,7 @@ async def chat_stopStream(
markdown_text: Optional[str] = None,
blocks: Optional[Union[str, Sequence[Union[Dict, Block]]]] = None,
metadata: Optional[Union[Dict, Metadata]] = None,
+ chunks: Optional[Sequence[Union[Dict, Chunk]]] = None,
**kwargs,
) -> AsyncSlackResponse:
"""Stops a streaming conversation.
@@ -2921,6 +2929,7 @@ async def chat_stopStream(
"markdown_text": markdown_text,
"blocks": blocks,
"metadata": metadata,
+ "chunks": chunks,
}
)
_parse_web_class_objects(kwargs)
diff --git a/slack_sdk/web/client.py b/slack_sdk/web/client.py
index dfa771832..1a70681a4 100644
--- a/slack_sdk/web/client.py
+++ b/slack_sdk/web/client.py
@@ -7,12 +7,13 @@
from typing import Any, Dict, List, Optional, Sequence, Union
import slack_sdk.errors as e
+from slack_sdk.models.messages.chunk import Chunk
from slack_sdk.models.views import View
from slack_sdk.web.chat_stream import ChatStream
from ..models.attachments import Attachment
from ..models.blocks import Block, RichTextBlock
-from ..models.metadata import Metadata, EntityMetadata, EventAndEntityMetadata
+from ..models.metadata import EntityMetadata, EventAndEntityMetadata, Metadata
from .base_client import BaseClient, SlackResponse
from .internal_utils import (
_parse_web_class_objects,
@@ -2621,6 +2622,7 @@ def chat_appendStream(
channel: str,
ts: str,
markdown_text: str,
+ chunks: Optional[Sequence[Union[Dict, Chunk]]] = None,
**kwargs,
) -> SlackResponse:
"""Appends text to an existing streaming conversation.
@@ -2631,8 +2633,10 @@ def chat_appendStream(
"channel": channel,
"ts": ts,
"markdown_text": markdown_text,
+ "chunks": chunks,
}
)
+ _parse_web_class_objects(kwargs)
kwargs = _remove_none_values(kwargs)
return self.api_call("chat.appendStream", json=kwargs)
@@ -2874,6 +2878,7 @@ def chat_startStream(
markdown_text: Optional[str] = None,
recipient_team_id: Optional[str] = None,
recipient_user_id: Optional[str] = None,
+ chunks: Optional[Sequence[Union[Dict, Chunk]]] = None,
**kwargs,
) -> SlackResponse:
"""Starts a new streaming conversation.
@@ -2886,8 +2891,10 @@ def chat_startStream(
"markdown_text": markdown_text,
"recipient_team_id": recipient_team_id,
"recipient_user_id": recipient_user_id,
+ "chunks": chunks,
}
)
+ _parse_web_class_objects(kwargs)
kwargs = _remove_none_values(kwargs)
return self.api_call("chat.startStream", json=kwargs)
@@ -2899,6 +2906,7 @@ def chat_stopStream(
markdown_text: Optional[str] = None,
blocks: Optional[Union[str, Sequence[Union[Dict, Block]]]] = None,
metadata: Optional[Union[Dict, Metadata]] = None,
+ chunks: Optional[Sequence[Union[Dict, Chunk]]] = None,
**kwargs,
) -> SlackResponse:
"""Stops a streaming conversation.
@@ -2911,6 +2919,7 @@ def chat_stopStream(
"markdown_text": markdown_text,
"blocks": blocks,
"metadata": metadata,
+ "chunks": chunks,
}
)
_parse_web_class_objects(kwargs)
diff --git a/slack_sdk/web/internal_utils.py b/slack_sdk/web/internal_utils.py
index 87139559c..ad23f87f8 100644
--- a/slack_sdk/web/internal_utils.py
+++ b/slack_sdk/web/internal_utils.py
@@ -11,13 +11,14 @@
from ssl import SSLContext
from typing import Any, Dict, Optional, Sequence, Union
from urllib.parse import urljoin
-from urllib.request import OpenerDirector, ProxyHandler, HTTPSHandler, Request, urlopen
+from urllib.request import HTTPSHandler, OpenerDirector, ProxyHandler, Request, urlopen
from slack_sdk import version
from slack_sdk.errors import SlackRequestError
from slack_sdk.models.attachments import Attachment
from slack_sdk.models.blocks import Block
-from slack_sdk.models.metadata import Metadata, EventAndEntityMetadata, EntityMetadata
+from slack_sdk.models.messages.chunk import Chunk
+from slack_sdk.models.metadata import EntityMetadata, EventAndEntityMetadata, Metadata
def convert_bool_to_0_or_1(params: Optional[Dict[str, Any]]) -> Optional[Dict[str, Any]]:
@@ -187,11 +188,13 @@ def _build_req_args(
def _parse_web_class_objects(kwargs) -> None:
- def to_dict(obj: Union[Dict, Block, Attachment, Metadata, EventAndEntityMetadata, EntityMetadata]):
+ def to_dict(obj: Union[Dict, Block, Attachment, Chunk, Metadata, EventAndEntityMetadata, EntityMetadata]):
if isinstance(obj, Block):
return obj.to_dict()
if isinstance(obj, Attachment):
return obj.to_dict()
+ if isinstance(obj, Chunk):
+ return obj.to_dict()
if isinstance(obj, Metadata):
return obj.to_dict()
if isinstance(obj, EventAndEntityMetadata):
@@ -211,6 +214,11 @@ def to_dict(obj: Union[Dict, Block, Attachment, Metadata, EventAndEntityMetadata
dict_attachments = [to_dict(a) for a in attachments]
kwargs.update({"attachments": dict_attachments})
+ chunks = kwargs.get("chunks", None)
+ if chunks is not None and isinstance(chunks, Sequence) and (not isinstance(chunks, str)):
+ dict_chunks = [to_dict(c) for c in chunks]
+ kwargs.update({"chunks": dict_chunks})
+
metadata = kwargs.get("metadata", None)
if metadata is not None and (
isinstance(metadata, Metadata)
diff --git a/slack_sdk/web/legacy_client.py b/slack_sdk/web/legacy_client.py
index df2bcc370..f11bbc495 100644
--- a/slack_sdk/web/legacy_client.py
+++ b/slack_sdk/web/legacy_client.py
@@ -19,11 +19,12 @@
from typing import Any, Dict, List, Optional, Sequence, Union
import slack_sdk.errors as e
+from slack_sdk.models.messages.chunk import Chunk
from slack_sdk.models.views import View
from ..models.attachments import Attachment
from ..models.blocks import Block, RichTextBlock
-from ..models.metadata import Metadata, EntityMetadata, EventAndEntityMetadata
+from ..models.metadata import EntityMetadata, EventAndEntityMetadata, Metadata
from .legacy_base_client import LegacyBaseClient, SlackResponse
from .internal_utils import (
_parse_web_class_objects,
@@ -2632,6 +2633,7 @@ def chat_appendStream(
channel: str,
ts: str,
markdown_text: str,
+ chunks: Optional[Sequence[Union[Dict, Chunk]]] = None,
**kwargs,
) -> Union[Future, SlackResponse]:
"""Appends text to an existing streaming conversation.
@@ -2642,8 +2644,10 @@ def chat_appendStream(
"channel": channel,
"ts": ts,
"markdown_text": markdown_text,
+ "chunks": chunks,
}
)
+ _parse_web_class_objects(kwargs)
kwargs = _remove_none_values(kwargs)
return self.api_call("chat.appendStream", json=kwargs)
@@ -2885,6 +2889,7 @@ def chat_startStream(
markdown_text: Optional[str] = None,
recipient_team_id: Optional[str] = None,
recipient_user_id: Optional[str] = None,
+ chunks: Optional[Sequence[Union[Dict, Chunk]]] = None,
**kwargs,
) -> Union[Future, SlackResponse]:
"""Starts a new streaming conversation.
@@ -2897,8 +2902,10 @@ def chat_startStream(
"markdown_text": markdown_text,
"recipient_team_id": recipient_team_id,
"recipient_user_id": recipient_user_id,
+ "chunks": chunks,
}
)
+ _parse_web_class_objects(kwargs)
kwargs = _remove_none_values(kwargs)
return self.api_call("chat.startStream", json=kwargs)
@@ -2910,6 +2917,7 @@ def chat_stopStream(
markdown_text: Optional[str] = None,
blocks: Optional[Union[str, Sequence[Union[Dict, Block]]]] = None,
metadata: Optional[Union[Dict, Metadata]] = None,
+ chunks: Optional[Sequence[Union[Dict, Chunk]]] = None,
**kwargs,
) -> Union[Future, SlackResponse]:
"""Stops a streaming conversation.
@@ -2922,6 +2930,7 @@ def chat_stopStream(
"markdown_text": markdown_text,
"blocks": blocks,
"metadata": metadata,
+ "chunks": chunks,
}
)
_parse_web_class_objects(kwargs)
diff --git a/tests/slack_sdk/models/test_chunks.py b/tests/slack_sdk/models/test_chunks.py
new file mode 100644
index 000000000..1b8b58c96
--- /dev/null
+++ b/tests/slack_sdk/models/test_chunks.py
@@ -0,0 +1,72 @@
+import unittest
+
+from slack_sdk.models.messages.chunk import MarkdownTextChunk, TaskUpdateChunk, URLSource
+
+
+class MarkdownTextChunkTests(unittest.TestCase):
+ def test_json(self):
+ self.assertDictEqual(
+ MarkdownTextChunk(text="greetings!").to_dict(),
+ {
+ "type": "markdown_text",
+ "text": "greetings!",
+ },
+ )
+
+
+class TaskUpdateChunkTests(unittest.TestCase):
+ def test_json(self):
+ self.assertDictEqual(
+ TaskUpdateChunk(id="001", title="Waiting...", status="pending").to_dict(),
+ {
+ "type": "task_update",
+ "id": "001",
+ "title": "Waiting...",
+ "status": "pending",
+ },
+ )
+ self.assertDictEqual(
+ TaskUpdateChunk(
+ id="002",
+ title="Wondering...",
+ status="in_progress",
+ details="- Gathering information...",
+ ).to_dict(),
+ {
+ "type": "task_update",
+ "id": "002",
+ "title": "Wondering...",
+ "status": "in_progress",
+ "details": "- Gathering information...",
+ },
+ )
+ self.assertDictEqual(
+ TaskUpdateChunk(
+ id="003",
+ title="Answering...",
+ status="complete",
+ output="Found a solution",
+ sources=[
+ URLSource(
+ text="The Free Encyclopedia",
+ url="https://wikipedia.org",
+ icon_url="https://example.com/globe.png",
+ ),
+ ],
+ ).to_dict(),
+ {
+ "type": "task_update",
+ "id": "003",
+ "title": "Answering...",
+ "status": "complete",
+ "output": "Found a solution",
+ "sources": [
+ {
+ "type": "url",
+ "text": "The Free Encyclopedia",
+ "url": "https://wikipedia.org",
+ "icon_url": "https://example.com/globe.png",
+ },
+ ],
+ },
+ )
diff --git a/tests/slack_sdk/web/test_internal_utils.py b/tests/slack_sdk/web/test_internal_utils.py
index ac7704b30..fc6574aab 100644
--- a/tests/slack_sdk/web/test_internal_utils.py
+++ b/tests/slack_sdk/web/test_internal_utils.py
@@ -2,18 +2,18 @@
import unittest
from io import BytesIO
from pathlib import Path
-from typing import Dict, Sequence, Union
+from typing import Dict
-import pytest
from slack_sdk.models.attachments import Attachment
from slack_sdk.models.blocks import Block, DividerBlock
+from slack_sdk.models.messages.chunk import MarkdownTextChunk, TaskUpdateChunk
from slack_sdk.web.internal_utils import (
_build_unexpected_body_error_message,
+ _get_url,
+ _next_cursor_is_present,
_parse_web_class_objects,
_to_v2_file_upload_item,
- _next_cursor_is_present,
- _get_url,
)
@@ -57,6 +57,20 @@ def test_can_parse_sequence_of_attachments(self):
for attachment in kwargs["attachments"]:
assert isinstance(attachment, Dict)
+ def test_can_parse_sequence_of_chunks(self):
+ for chunks in [
+ [MarkdownTextChunk(text="fiz"), TaskUpdateChunk(id="001", title="baz", status="complete")], # list
+ (
+ MarkdownTextChunk(text="fiz"),
+ TaskUpdateChunk(id="001", title="baz", status="complete"),
+ ), # tuple
+ ]:
+ kwargs = {"chunks": chunks}
+ _parse_web_class_objects(kwargs)
+ assert kwargs["chunks"]
+ for chunks in kwargs["chunks"]:
+ assert isinstance(chunks, Dict)
+
def test_can_parse_str_blocks(self):
input = json.dumps([Block(block_id="42").to_dict(), Block(block_id="24").to_dict()])
kwargs = {"blocks": input}
@@ -71,6 +85,15 @@ def test_can_parse_str_attachments(self):
assert isinstance(kwargs["attachments"], str)
assert input == kwargs["attachments"]
+ def test_can_parse_str_chunks(self):
+ input = json.dumps(
+ [MarkdownTextChunk(text="fiz").to_dict(), TaskUpdateChunk(id="001", title="baz", status="complete").to_dict()]
+ )
+ kwargs = {"chunks": input}
+ _parse_web_class_objects(kwargs)
+ assert isinstance(kwargs["chunks"], str)
+ assert input == kwargs["chunks"]
+
def test_can_parse_user_auth_blocks(self):
kwargs = {
"channel": "C12345",
From f4c0182be7daf7c1c14552763dd204c33f2b64d6 Mon Sep 17 00:00:00 2001
From: Eden Zimbelman
Date: Fri, 16 Jan 2026 15:25:46 -0800
Subject: [PATCH 09/13] feat: support and flush chunks in the chat stream
helper (#1809)
---
slack_sdk/web/async_chat_stream.py | 36 +++++---
slack_sdk/web/async_client.py | 2 +-
slack_sdk/web/chat_stream.py | 36 +++++---
slack_sdk/web/client.py | 2 +-
slack_sdk/web/legacy_client.py | 2 +-
tests/slack_sdk/web/test_chat_stream.py | 82 +++++++++++++++++-
.../web/test_async_chat_stream.py | 83 ++++++++++++++++++-
7 files changed, 212 insertions(+), 31 deletions(-)
diff --git a/slack_sdk/web/async_chat_stream.py b/slack_sdk/web/async_chat_stream.py
index 4661f19dd..550902182 100644
--- a/slack_sdk/web/async_chat_stream.py
+++ b/slack_sdk/web/async_chat_stream.py
@@ -10,10 +10,11 @@
import json
import logging
-from typing import TYPE_CHECKING, Dict, Optional, Sequence, Union
+from typing import TYPE_CHECKING, Dict, List, Optional, Sequence, Union
import slack_sdk.errors as e
from slack_sdk.models.blocks.blocks import Block
+from slack_sdk.models.messages.chunk import Chunk, MarkdownTextChunk
from slack_sdk.models.metadata import Metadata
from slack_sdk.web.async_slack_response import AsyncSlackResponse
@@ -75,7 +76,8 @@ def __init__(
async def append(
self,
*,
- markdown_text: str,
+ markdown_text: Optional[str] = None,
+ chunks: Optional[Sequence[Union[Dict, Chunk]]] = None,
**kwargs,
) -> Optional[AsyncSlackResponse]:
"""Append to the stream.
@@ -84,6 +86,7 @@ async def append(
is stopped this method cannot be called.
Args:
+ chunks: An array of streaming chunks that can contain either markdown text or task updates.
markdown_text: Accepts message text formatted in markdown. Limit this field to 12,000 characters. This text is
what will be appended to the message received so far.
**kwargs: Additional arguments passed to the underlying API calls.
@@ -111,9 +114,10 @@ async def append(
raise e.SlackRequestError(f"Cannot append to stream: stream state is {self._state}")
if kwargs.get("token"):
self._token = kwargs.pop("token")
- self._buffer += markdown_text
- if len(self._buffer) >= self._buffer_size:
- return await self._flush_buffer(**kwargs)
+ if markdown_text is not None:
+ self._buffer += markdown_text
+ if len(self._buffer) >= self._buffer_size or chunks is not None:
+ return await self._flush_buffer(chunks=chunks, **kwargs)
details = {
"buffer_length": len(self._buffer),
"buffer_size": self._buffer_size,
@@ -129,6 +133,7 @@ async def stop(
self,
*,
markdown_text: Optional[str] = None,
+ chunks: Optional[Sequence[Union[Dict, Chunk]]] = None,
blocks: Optional[Union[str, Sequence[Union[Dict, Block]]]] = None,
metadata: Optional[Union[Dict, Metadata]] = None,
**kwargs,
@@ -137,6 +142,7 @@ async def stop(
Args:
blocks: A list of blocks that will be rendered at the bottom of the finalized message.
+ chunks: An array of streaming chunks that can contain either markdown text or task updates.
markdown_text: Accepts message text formatted in markdown. Limit this field to 12,000 characters. This text is
what will be appended to the message received so far.
metadata: JSON object with event_type and event_payload fields, presented as a URL-encoded string. Metadata you
@@ -177,26 +183,36 @@ async def stop(
raise e.SlackRequestError("Failed to stop stream: stream not started")
self._stream_ts = str(response["ts"])
self._state = "in_progress"
+ flushings: List[Union[Dict, Chunk]] = []
+ if len(self._buffer) != 0:
+ flushings.append(MarkdownTextChunk(text=self._buffer))
+ if chunks is not None:
+ flushings.extend(chunks)
response = await self._client.chat_stopStream(
token=self._token,
channel=self._stream_args["channel"],
ts=self._stream_ts,
blocks=blocks,
- markdown_text=self._buffer,
+ chunks=flushings,
metadata=metadata,
**kwargs,
)
self._state = "completed"
return response
- async def _flush_buffer(self, **kwargs) -> AsyncSlackResponse:
- """Flush the internal buffer by making appropriate API calls."""
+ async def _flush_buffer(self, chunks: Optional[Sequence[Union[Dict, Chunk]]] = None, **kwargs) -> AsyncSlackResponse:
+ """Flush the internal buffer with chunks by making appropriate API calls."""
+ flushings: List[Union[Dict, Chunk]] = []
+ if len(self._buffer) != 0:
+ flushings.append(MarkdownTextChunk(text=self._buffer))
+ if chunks is not None:
+ flushings.extend(chunks)
if not self._stream_ts:
response = await self._client.chat_startStream(
**self._stream_args,
token=self._token,
**kwargs,
- markdown_text=self._buffer,
+ chunks=flushings,
)
self._stream_ts = response.get("ts")
self._state = "in_progress"
@@ -206,7 +222,7 @@ async def _flush_buffer(self, **kwargs) -> AsyncSlackResponse:
channel=self._stream_args["channel"],
ts=self._stream_ts,
**kwargs,
- markdown_text=self._buffer,
+ chunks=flushings,
)
self._buffer = ""
return response
diff --git a/slack_sdk/web/async_client.py b/slack_sdk/web/async_client.py
index 0a9f702b9..5aaa2c610 100644
--- a/slack_sdk/web/async_client.py
+++ b/slack_sdk/web/async_client.py
@@ -2631,7 +2631,7 @@ async def chat_appendStream(
*,
channel: str,
ts: str,
- markdown_text: str,
+ markdown_text: Optional[str] = None,
chunks: Optional[Sequence[Union[Dict, Chunk]]] = None,
**kwargs,
) -> AsyncSlackResponse:
diff --git a/slack_sdk/web/chat_stream.py b/slack_sdk/web/chat_stream.py
index 1a379c9cb..acdac728a 100644
--- a/slack_sdk/web/chat_stream.py
+++ b/slack_sdk/web/chat_stream.py
@@ -1,9 +1,10 @@
import json
import logging
-from typing import TYPE_CHECKING, Dict, Optional, Sequence, Union
+from typing import TYPE_CHECKING, Dict, List, Optional, Sequence, Union
import slack_sdk.errors as e
from slack_sdk.models.blocks.blocks import Block
+from slack_sdk.models.messages.chunk import Chunk, MarkdownTextChunk
from slack_sdk.models.metadata import Metadata
from slack_sdk.web.slack_response import SlackResponse
@@ -65,7 +66,8 @@ def __init__(
def append(
self,
*,
- markdown_text: str,
+ markdown_text: Optional[str] = None,
+ chunks: Optional[Sequence[Union[Dict, Chunk]]] = None,
**kwargs,
) -> Optional[SlackResponse]:
"""Append to the stream.
@@ -74,6 +76,7 @@ def append(
is stopped this method cannot be called.
Args:
+ chunks: An array of streaming chunks that can contain either markdown text or task updates.
markdown_text: Accepts message text formatted in markdown. Limit this field to 12,000 characters. This text is
what will be appended to the message received so far.
**kwargs: Additional arguments passed to the underlying API calls.
@@ -101,9 +104,10 @@ def append(
raise e.SlackRequestError(f"Cannot append to stream: stream state is {self._state}")
if kwargs.get("token"):
self._token = kwargs.pop("token")
- self._buffer += markdown_text
- if len(self._buffer) >= self._buffer_size:
- return self._flush_buffer(**kwargs)
+ if markdown_text is not None:
+ self._buffer += markdown_text
+ if len(self._buffer) >= self._buffer_size or chunks is not None:
+ return self._flush_buffer(chunks=chunks, **kwargs)
details = {
"buffer_length": len(self._buffer),
"buffer_size": self._buffer_size,
@@ -119,6 +123,7 @@ def stop(
self,
*,
markdown_text: Optional[str] = None,
+ chunks: Optional[Sequence[Union[Dict, Chunk]]] = None,
blocks: Optional[Union[str, Sequence[Union[Dict, Block]]]] = None,
metadata: Optional[Union[Dict, Metadata]] = None,
**kwargs,
@@ -127,6 +132,7 @@ def stop(
Args:
blocks: A list of blocks that will be rendered at the bottom of the finalized message.
+ chunks: An array of streaming chunks that can contain either markdown text or task updates.
markdown_text: Accepts message text formatted in markdown. Limit this field to 12,000 characters. This text is
what will be appended to the message received so far.
metadata: JSON object with event_type and event_payload fields, presented as a URL-encoded string. Metadata you
@@ -167,26 +173,36 @@ def stop(
raise e.SlackRequestError("Failed to stop stream: stream not started")
self._stream_ts = str(response["ts"])
self._state = "in_progress"
+ flushings: List[Union[Dict, Chunk]] = []
+ if len(self._buffer) != 0:
+ flushings.append(MarkdownTextChunk(text=self._buffer))
+ if chunks is not None:
+ flushings.extend(chunks)
response = self._client.chat_stopStream(
token=self._token,
channel=self._stream_args["channel"],
ts=self._stream_ts,
blocks=blocks,
- markdown_text=self._buffer,
+ chunks=flushings,
metadata=metadata,
**kwargs,
)
self._state = "completed"
return response
- def _flush_buffer(self, **kwargs) -> SlackResponse:
- """Flush the internal buffer by making appropriate API calls."""
+ def _flush_buffer(self, chunks: Optional[Sequence[Union[Dict, Chunk]]] = None, **kwargs) -> SlackResponse:
+ """Flush the internal buffer with chunks by making appropriate API calls."""
+ flushings: List[Union[Dict, Chunk]] = []
+ if len(self._buffer) != 0:
+ flushings.append(MarkdownTextChunk(text=self._buffer))
+ if chunks is not None:
+ flushings.extend(chunks)
if not self._stream_ts:
response = self._client.chat_startStream(
**self._stream_args,
token=self._token,
**kwargs,
- markdown_text=self._buffer,
+ chunks=flushings,
)
self._stream_ts = response.get("ts")
self._state = "in_progress"
@@ -196,7 +212,7 @@ def _flush_buffer(self, **kwargs) -> SlackResponse:
channel=self._stream_args["channel"],
ts=self._stream_ts,
**kwargs,
- markdown_text=self._buffer,
+ chunks=flushings,
)
self._buffer = ""
return response
diff --git a/slack_sdk/web/client.py b/slack_sdk/web/client.py
index 1a70681a4..392a261ad 100644
--- a/slack_sdk/web/client.py
+++ b/slack_sdk/web/client.py
@@ -2621,7 +2621,7 @@ def chat_appendStream(
*,
channel: str,
ts: str,
- markdown_text: str,
+ markdown_text: Optional[str] = None,
chunks: Optional[Sequence[Union[Dict, Chunk]]] = None,
**kwargs,
) -> SlackResponse:
diff --git a/slack_sdk/web/legacy_client.py b/slack_sdk/web/legacy_client.py
index f11bbc495..7bb0609c5 100644
--- a/slack_sdk/web/legacy_client.py
+++ b/slack_sdk/web/legacy_client.py
@@ -2632,7 +2632,7 @@ def chat_appendStream(
*,
channel: str,
ts: str,
- markdown_text: str,
+ markdown_text: Optional[str] = None,
chunks: Optional[Sequence[Union[Dict, Chunk]]] = None,
**kwargs,
) -> Union[Future, SlackResponse]:
diff --git a/tests/slack_sdk/web/test_chat_stream.py b/tests/slack_sdk/web/test_chat_stream.py
index 75c13c8c2..a6d846769 100644
--- a/tests/slack_sdk/web/test_chat_stream.py
+++ b/tests/slack_sdk/web/test_chat_stream.py
@@ -7,6 +7,7 @@
from slack_sdk.models.blocks.basic_components import FeedbackButtonObject
from slack_sdk.models.blocks.block_elements import FeedbackButtonsElement, IconButtonElement
from slack_sdk.models.blocks.blocks import ContextActionsBlock
+from slack_sdk.models.messages.chunk import MarkdownTextChunk, TaskUpdateChunk
from tests.mock_web_api_server import cleanup_mock_web_api_server, setup_mock_web_api_server
from tests.slack_sdk.web.mock_web_api_handler import MockHandler
@@ -105,7 +106,10 @@ def test_streams_a_short_message(self):
stop_request = self.thread.server.chat_stream_requests.get("/chat.stopStream", {})
self.assertEqual(stop_request.get("channel"), "C0123456789")
self.assertEqual(stop_request.get("ts"), "123.123")
- self.assertEqual(stop_request.get("markdown_text"), "nice!")
+ self.assertEqual(
+ json.dumps(stop_request.get("chunks")),
+ '[{"text": "nice!", "type": "markdown_text"}]',
+ )
def test_streams_a_long_message(self):
streamer = self.client.chat_stream(
@@ -146,13 +150,19 @@ def test_streams_a_long_message(self):
start_request = self.thread.server.chat_stream_requests.get("/chat.startStream", {})
self.assertEqual(start_request.get("channel"), "C0123456789")
self.assertEqual(start_request.get("thread_ts"), "123.000")
- self.assertEqual(start_request.get("markdown_text"), "**this messag")
+ self.assertEqual(
+ json.dumps(start_request.get("chunks")),
+ '[{"text": "**this messag", "type": "markdown_text"}]',
+ )
self.assertEqual(start_request.get("recipient_team_id"), "T0123456789")
self.assertEqual(start_request.get("recipient_user_id"), "U0123456789")
append_request = self.thread.server.chat_stream_requests.get("/chat.appendStream", {})
self.assertEqual(append_request.get("channel"), "C0123456789")
- self.assertEqual(append_request.get("markdown_text"), "e is bold!")
+ self.assertEqual(
+ json.dumps(append_request.get("chunks")),
+ '[{"text": "e is bold!", "type": "markdown_text"}]',
+ )
self.assertEqual(append_request.get("token"), "xoxb-chat_stream_test_token1")
self.assertEqual(append_request.get("ts"), "123.123")
@@ -162,10 +172,74 @@ def test_streams_a_long_message(self):
'[{"elements": [{"negative_button": {"text": {"emoji": true, "text": "bad", "type": "plain_text"}, "value": "-1"}, "positive_button": {"text": {"emoji": true, "text": "good", "type": "plain_text"}, "value": "+1"}, "type": "feedback_buttons"}, {"icon": "trash", "text": {"emoji": true, "text": "delete", "type": "plain_text"}, "type": "icon_button"}], "type": "context_actions"}]',
)
self.assertEqual(stop_request.get("channel"), "C0123456789")
- self.assertEqual(stop_request.get("markdown_text"), "**")
+ self.assertEqual(
+ json.dumps(stop_request.get("chunks")),
+ '[{"text": "**", "type": "markdown_text"}]',
+ )
self.assertEqual(stop_request.get("token"), "xoxb-chat_stream_test_token2")
self.assertEqual(stop_request.get("ts"), "123.123")
+ def test_streams_a_chunk_message(self):
+ streamer = self.client.chat_stream(
+ channel="C0123456789",
+ recipient_team_id="T0123456789",
+ recipient_user_id="U0123456789",
+ thread_ts="123.000",
+ )
+ streamer.append(markdown_text="**this is ")
+ streamer.append(markdown_text="buffered**")
+ streamer.append(
+ chunks=[
+ TaskUpdateChunk(
+ id="001",
+ title="Counting...",
+ status="pending",
+ ),
+ ],
+ )
+ streamer.append(
+ chunks=[
+ MarkdownTextChunk(text="**this is unbuffered**"),
+ ],
+ )
+ streamer.append(markdown_text="\n")
+ streamer.stop(
+ chunks=[
+ MarkdownTextChunk(text=":space_invader:"),
+ ],
+ )
+
+ self.assertEqual(self.received_requests.get("/chat.startStream", 0), 1)
+ self.assertEqual(self.received_requests.get("/chat.appendStream", 0), 1)
+ self.assertEqual(self.received_requests.get("/chat.stopStream", 0), 1)
+
+ if hasattr(self.thread.server, "chat_stream_requests"):
+ start_request = self.thread.server.chat_stream_requests.get("/chat.startStream", {})
+ self.assertEqual(start_request.get("channel"), "C0123456789")
+ self.assertEqual(start_request.get("thread_ts"), "123.000")
+ self.assertEqual(
+ json.dumps(start_request.get("chunks")),
+ '[{"text": "**this is buffered**", "type": "markdown_text"}, {"id": "001", "status": "pending", "title": "Counting...", "type": "task_update"}]',
+ )
+ self.assertEqual(start_request.get("recipient_team_id"), "T0123456789")
+ self.assertEqual(start_request.get("recipient_user_id"), "U0123456789")
+
+ append_request = self.thread.server.chat_stream_requests.get("/chat.appendStream", {})
+ self.assertEqual(append_request.get("channel"), "C0123456789")
+ self.assertEqual(append_request.get("ts"), "123.123")
+ self.assertEqual(
+ json.dumps(append_request.get("chunks")),
+ '[{"text": "**this is unbuffered**", "type": "markdown_text"}]',
+ )
+
+ stop_request = self.thread.server.chat_stream_requests.get("/chat.stopStream", {})
+ self.assertEqual(stop_request.get("channel"), "C0123456789")
+ self.assertEqual(stop_request.get("ts"), "123.123")
+ self.assertEqual(
+ json.dumps(stop_request.get("chunks")),
+ '[{"text": "\\n", "type": "markdown_text"}, {"text": ":space_invader:", "type": "markdown_text"}]',
+ )
+
def test_streams_errors_when_appending_to_an_unstarted_stream(self):
streamer = self.client.chat_stream(
channel="C0123456789",
diff --git a/tests/slack_sdk_async/web/test_async_chat_stream.py b/tests/slack_sdk_async/web/test_async_chat_stream.py
index 212fee1e2..2a4f5b931 100644
--- a/tests/slack_sdk_async/web/test_async_chat_stream.py
+++ b/tests/slack_sdk_async/web/test_async_chat_stream.py
@@ -6,6 +6,7 @@
from slack_sdk.models.blocks.basic_components import FeedbackButtonObject
from slack_sdk.models.blocks.block_elements import FeedbackButtonsElement, IconButtonElement
from slack_sdk.models.blocks.blocks import ContextActionsBlock
+from slack_sdk.models.messages.chunk import MarkdownTextChunk, TaskUpdateChunk
from slack_sdk.web.async_client import AsyncWebClient
from tests.mock_web_api_server import cleanup_mock_web_api_server, setup_mock_web_api_server
from tests.slack_sdk.web.mock_web_api_handler import MockHandler
@@ -107,7 +108,10 @@ async def test_streams_a_short_message(self):
stop_request = self.thread.server.chat_stream_requests.get("/chat.stopStream", {})
self.assertEqual(stop_request.get("channel"), "C0123456789")
self.assertEqual(stop_request.get("ts"), "123.123")
- self.assertEqual(stop_request.get("markdown_text"), "nice!")
+ self.assertEqual(
+ json.dumps(stop_request.get("chunks")),
+ '[{"text": "nice!", "type": "markdown_text"}]',
+ )
@async_test
async def test_streams_a_long_message(self):
@@ -149,13 +153,19 @@ async def test_streams_a_long_message(self):
start_request = self.thread.server.chat_stream_requests.get("/chat.startStream", {})
self.assertEqual(start_request.get("channel"), "C0123456789")
self.assertEqual(start_request.get("thread_ts"), "123.000")
- self.assertEqual(start_request.get("markdown_text"), "**this messag")
+ self.assertEqual(
+ json.dumps(start_request.get("chunks")),
+ '[{"text": "**this messag", "type": "markdown_text"}]',
+ )
self.assertEqual(start_request.get("recipient_team_id"), "T0123456789")
self.assertEqual(start_request.get("recipient_user_id"), "U0123456789")
append_request = self.thread.server.chat_stream_requests.get("/chat.appendStream", {})
self.assertEqual(append_request.get("channel"), "C0123456789")
- self.assertEqual(append_request.get("markdown_text"), "e is bold!")
+ self.assertEqual(
+ json.dumps(append_request.get("chunks")),
+ '[{"text": "e is bold!", "type": "markdown_text"}]',
+ )
self.assertEqual(append_request.get("token"), "xoxb-chat_stream_test_token1")
self.assertEqual(append_request.get("ts"), "123.123")
@@ -165,10 +175,75 @@ async def test_streams_a_long_message(self):
'[{"elements": [{"negative_button": {"text": {"emoji": true, "text": "bad", "type": "plain_text"}, "value": "-1"}, "positive_button": {"text": {"emoji": true, "text": "good", "type": "plain_text"}, "value": "+1"}, "type": "feedback_buttons"}, {"icon": "trash", "text": {"emoji": true, "text": "delete", "type": "plain_text"}, "type": "icon_button"}], "type": "context_actions"}]',
)
self.assertEqual(stop_request.get("channel"), "C0123456789")
- self.assertEqual(stop_request.get("markdown_text"), "**")
+ self.assertEqual(
+ json.dumps(stop_request.get("chunks")),
+ '[{"text": "**", "type": "markdown_text"}]',
+ )
self.assertEqual(stop_request.get("token"), "xoxb-chat_stream_test_token2")
self.assertEqual(stop_request.get("ts"), "123.123")
+ @async_test
+ async def test_streams_a_chunk_message(self):
+ streamer = await self.client.chat_stream(
+ channel="C0123456789",
+ recipient_team_id="T0123456789",
+ recipient_user_id="U0123456789",
+ thread_ts="123.000",
+ )
+ await streamer.append(markdown_text="**this is ")
+ await streamer.append(markdown_text="buffered**")
+ await streamer.append(
+ chunks=[
+ TaskUpdateChunk(
+ id="001",
+ title="Counting...",
+ status="pending",
+ ),
+ ],
+ )
+ await streamer.append(
+ chunks=[
+ MarkdownTextChunk(text="**this is unbuffered**"),
+ ],
+ )
+ await streamer.append(markdown_text="\n")
+ await streamer.stop(
+ chunks=[
+ MarkdownTextChunk(text=":space_invader:"),
+ ],
+ )
+
+ self.assertEqual(self.received_requests.get("/chat.startStream", 0), 1)
+ self.assertEqual(self.received_requests.get("/chat.appendStream", 0), 1)
+ self.assertEqual(self.received_requests.get("/chat.stopStream", 0), 1)
+
+ if hasattr(self.thread.server, "chat_stream_requests"):
+ start_request = self.thread.server.chat_stream_requests.get("/chat.startStream", {})
+ self.assertEqual(start_request.get("channel"), "C0123456789")
+ self.assertEqual(start_request.get("thread_ts"), "123.000")
+ self.assertEqual(
+ json.dumps(start_request.get("chunks")),
+ '[{"text": "**this is buffered**", "type": "markdown_text"}, {"id": "001", "status": "pending", "title": "Counting...", "type": "task_update"}]',
+ )
+ self.assertEqual(start_request.get("recipient_team_id"), "T0123456789")
+ self.assertEqual(start_request.get("recipient_user_id"), "U0123456789")
+
+ append_request = self.thread.server.chat_stream_requests.get("/chat.appendStream", {})
+ self.assertEqual(append_request.get("channel"), "C0123456789")
+ self.assertEqual(append_request.get("ts"), "123.123")
+ self.assertEqual(
+ json.dumps(append_request.get("chunks")),
+ '[{"text": "**this is unbuffered**", "type": "markdown_text"}]',
+ )
+
+ stop_request = self.thread.server.chat_stream_requests.get("/chat.stopStream", {})
+ self.assertEqual(stop_request.get("channel"), "C0123456789")
+ self.assertEqual(stop_request.get("ts"), "123.123")
+ self.assertEqual(
+ json.dumps(stop_request.get("chunks")),
+ '[{"text": "\\n", "type": "markdown_text"}, {"text": ":space_invader:", "type": "markdown_text"}]',
+ )
+
@async_test
async def test_streams_errors_when_appending_to_an_unstarted_stream(self):
streamer = await self.client.chat_stream(
From b0311489b27f76e91d7bde515e163c110af2c7c1 Mon Sep 17 00:00:00 2001
From: Eden Zimbelman
Date: Fri, 16 Jan 2026 15:50:24 -0800
Subject: [PATCH 10/13] feat: add task_card and plan blocks (#1819)
---
slack_sdk/models/blocks/__init__.py | 6 ++
slack_sdk/models/blocks/block_elements.py | 42 +++++++++
slack_sdk/models/blocks/blocks.py | 106 ++++++++++++++++++++++
slack_sdk/models/messages/chunk.py | 54 +----------
tests/slack_sdk/models/test_blocks.py | 83 +++++++++++++++++
tests/slack_sdk/models/test_chunks.py | 14 ++-
6 files changed, 253 insertions(+), 52 deletions(-)
diff --git a/slack_sdk/models/blocks/__init__.py b/slack_sdk/models/blocks/__init__.py
index d2776a9dc..b8592c2e9 100644
--- a/slack_sdk/models/blocks/__init__.py
+++ b/slack_sdk/models/blocks/__init__.py
@@ -55,6 +55,7 @@
StaticSelectElement,
TimePickerElement,
UrlInputElement,
+ UrlSourceElement,
UserMultiSelectElement,
UserSelectElement,
)
@@ -70,9 +71,11 @@
ImageBlock,
InputBlock,
MarkdownBlock,
+ PlanBlock,
RichTextBlock,
SectionBlock,
TableBlock,
+ TaskCardBlock,
VideoBlock,
)
@@ -111,6 +114,7 @@
"PlainTextInputElement",
"EmailInputElement",
"UrlInputElement",
+ "UrlSourceElement",
"NumberInputElement",
"RadioButtonsElement",
"SelectElement",
@@ -135,8 +139,10 @@
"ImageBlock",
"InputBlock",
"MarkdownBlock",
+ "PlanBlock",
"SectionBlock",
"TableBlock",
+ "TaskCardBlock",
"VideoBlock",
"RichTextBlock",
]
diff --git a/slack_sdk/models/blocks/block_elements.py b/slack_sdk/models/blocks/block_elements.py
index 89f0a7994..be683162b 100644
--- a/slack_sdk/models/blocks/block_elements.py
+++ b/slack_sdk/models/blocks/block_elements.py
@@ -1654,6 +1654,48 @@ def __init__(
self.dispatch_action_config = dispatch_action_config
+# -------------------------------------------------
+# Url Source Element
+# -------------------------------------------------
+
+
+class UrlSourceElement(BlockElement):
+ type = "url"
+
+ @property
+ def attributes(self) -> Set[str]: # type: ignore[override]
+ return super().attributes.union(
+ {
+ "url",
+ "text",
+ "icon_url",
+ }
+ )
+
+ def __init__(
+ self,
+ *,
+ url: str,
+ text: str,
+ icon_url: Optional[str] = None,
+ **others: Dict,
+ ):
+ """
+ A URL source element to reference in a task card block.
+ https://docs.slack.dev/reference/block-kit/block-elements/url-source-element
+
+ Args:
+ url (required): The URL type source.
+ text (required): Display text for the URL.
+ icon_url: Optional icon URL to display with the source.
+ """
+ super().__init__(type=self.type)
+ show_unknown_key_warning(self, others)
+ self.url = url
+ self.text = text
+ self.icon_url = icon_url
+
+
# -------------------------------------------------
# Number Input Element
# -------------------------------------------------
diff --git a/slack_sdk/models/blocks/blocks.py b/slack_sdk/models/blocks/blocks.py
index cac463c99..26c25d73f 100644
--- a/slack_sdk/models/blocks/blocks.py
+++ b/slack_sdk/models/blocks/blocks.py
@@ -16,6 +16,7 @@
InputInteractiveElement,
InteractiveElement,
RichTextElement,
+ UrlSourceElement,
)
# -------------------------------------------------
@@ -97,6 +98,10 @@ def parse(cls, block: Union[dict, "Block"]) -> Optional["Block"]:
return RichTextBlock(**block)
elif type == TableBlock.type:
return TableBlock(**block)
+ elif type == TaskCardBlock.type:
+ return TaskCardBlock(**block)
+ elif type == PlanBlock.type:
+ return PlanBlock(**block)
else:
cls.logger.warning(f"Unknown block detected and skipped ({block})")
return None
@@ -777,3 +782,104 @@ def __init__(
@JsonValidator("rows attribute must be specified")
def _validate_rows(self):
return self.rows is not None and len(self.rows) > 0
+
+
+class TaskCardBlock(Block):
+ type = "task_card"
+
+ @property
+ def attributes(self) -> Set[str]: # type: ignore[override]
+ return super().attributes.union(
+ {
+ "task_id",
+ "title",
+ "details",
+ "output",
+ "sources",
+ "status",
+ }
+ )
+
+ def __init__(
+ self,
+ *,
+ task_id: str,
+ title: str,
+ details: Optional[Union[RichTextBlock, dict]] = None,
+ output: Optional[Union[RichTextBlock, dict]] = None,
+ sources: Optional[Sequence[Union[UrlSourceElement, dict]]] = None,
+ status: str, # pending, in_progress, complete, error
+ block_id: Optional[str] = None,
+ **others: dict,
+ ):
+ """A discrete action or tool call.
+ https://docs.slack.dev/reference/block-kit/blocks/task-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.
+ block_id should be unique for each message and each iteration of a message.
+ If a message is updated, use a new block_id.
+ task_id (required): ID for the task
+ title (required): Title of the task in plain text
+ details: Details of the task in the form of a single "rich_text" entity.
+ output: Output of the task in the form of a single "rich_text" entity.
+ sources: List of sources used to generate a response
+ status: The state of a task. Either "pending" or "in_progress" or "complete" or "error".
+ """
+ super().__init__(type=self.type, block_id=block_id)
+ show_unknown_key_warning(self, others)
+
+ self.task_id = task_id
+ self.title = title
+ self.details = details
+ self.output = output
+ self.sources = sources
+ self.status = status
+
+ @JsonValidator("status must be an expected value (pending, in_progress, complete, or error)")
+ def _validate_rows(self):
+ return self.status in ["pending", "in_progress", "complete", "error"]
+
+
+class PlanBlock(Block):
+ type = "plan"
+
+ @property
+ def attributes(self) -> Set[str]: # type: ignore[override]
+ return super().attributes.union(
+ {
+ "plan_id",
+ "title",
+ "tasks",
+ }
+ )
+
+ def __init__(
+ self,
+ *,
+ plan_id: str,
+ title: str,
+ tasks: Optional[Sequence[Union[Dict, TaskCardBlock]]] = None,
+ block_id: Optional[str] = None,
+ **others: dict,
+ ):
+ """A collection of related tasks.
+ https://docs.slack.dev/reference/block-kit/blocks/plan-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.
+ block_id should be unique for each message and each iteration of a message.
+ If a message is updated, use a new block_id.
+ plan_id (required): ID for the plan (May be removed / made optional, feel free to pass in a random UUID
+ for now)
+ title (required): Title of the plan in plain text
+ tasks: Details of the task in the form of a single "rich_text" entity.
+ """
+ super().__init__(type=self.type, block_id=block_id)
+ show_unknown_key_warning(self, others)
+
+ self.plan_id = plan_id
+ self.title = title
+ self.tasks = tasks
diff --git a/slack_sdk/models/messages/chunk.py b/slack_sdk/models/messages/chunk.py
index 837714af0..b00daebd1 100644
--- a/slack_sdk/models/messages/chunk.py
+++ b/slack_sdk/models/messages/chunk.py
@@ -1,9 +1,9 @@
import logging
-from typing import Any, Dict, Optional, Sequence, Set, Union
+from typing import Dict, Optional, Sequence, Set, Union
-from slack_sdk.errors import SlackObjectFormationError
from slack_sdk.models import show_unknown_key_warning
from slack_sdk.models.basic_objects import JsonObject
+from slack_sdk.models.blocks.block_elements import UrlSourceElement
class Chunk(JsonObject):
@@ -67,44 +67,6 @@ def __init__(
self.text = text
-class URLSource(JsonObject):
- type = "url"
-
- @property
- def attributes(self) -> Set[str]:
- return super().attributes.union(
- {
- "url",
- "text",
- "icon_url",
- }
- )
-
- def __init__(
- self,
- *,
- url: str,
- text: str,
- icon_url: Optional[str] = None,
- **others: Dict,
- ):
- show_unknown_key_warning(self, others)
- self._url = url
- self._text = text
- self._icon_url = icon_url
-
- def to_dict(self) -> Dict[str, Any]:
- self.validate_json()
- json: Dict[str, Union[str, Dict]] = {
- "type": self.type,
- "url": self._url,
- "text": self._text,
- }
- if self._icon_url:
- json["icon_url"] = self._icon_url
- return json
-
-
class TaskUpdateChunk(Chunk):
type = "task_update"
@@ -129,7 +91,7 @@ def __init__(
status: str, # "pending", "in_progress", "complete", "error"
details: Optional[str] = None,
output: Optional[str] = None,
- sources: Optional[Sequence[Union[Dict, URLSource]]] = None,
+ sources: Optional[Sequence[Union[Dict, UrlSourceElement]]] = None,
**others: Dict,
):
"""Used for displaying tool execution progress in a timeline-style UI.
@@ -144,12 +106,4 @@ def __init__(
self.status = status
self.details = details
self.output = output
- if sources is not None:
- self.sources = []
- for src in sources:
- if isinstance(src, Dict):
- self.sources.append(src)
- elif isinstance(src, URLSource):
- self.sources.append(src.to_dict())
- else:
- raise SlackObjectFormationError(f"Unsupported type for source in task update chunk: {type(src)}")
+ self.sources = sources
diff --git a/tests/slack_sdk/models/test_blocks.py b/tests/slack_sdk/models/test_blocks.py
index 6f3b9f141..daada60dd 100644
--- a/tests/slack_sdk/models/test_blocks.py
+++ b/tests/slack_sdk/models/test_blocks.py
@@ -21,6 +21,7 @@
Option,
OverflowMenuElement,
PlainTextObject,
+ PlanBlock,
RawTextObject,
RichTextBlock,
RichTextElementParts,
@@ -31,6 +32,7 @@
SectionBlock,
StaticSelectElement,
TableBlock,
+ TaskCardBlock,
VideoBlock,
)
from slack_sdk.models.blocks.basic_components import FeedbackButtonObject, SlackFile
@@ -890,6 +892,87 @@ def test_text_length_12001(self):
MarkdownBlock(**input).validate_json()
+# ----------------------------------------------
+# Plan
+# ----------------------------------------------
+
+
+class PlanBlockTests(unittest.TestCase):
+ def test_document(self):
+ input = {
+ "type": "plan",
+ "plan_id": "plan_1",
+ "title": "Thinking completed",
+ "tasks": [
+ {
+ "task_id": "call_001",
+ "title": "Fetched user profile information",
+ "status": "in_progress",
+ "details": {
+ "type": "rich_text",
+ "elements": [
+ {"type": "rich_text_section", "elements": [{"type": "text", "text": "Searched database..."}]}
+ ],
+ },
+ "output": {
+ "type": "rich_text",
+ "elements": [
+ {"type": "rich_text_section", "elements": [{"type": "text", "text": "Profile data loaded"}]}
+ ],
+ },
+ },
+ {
+ "task_id": "call_002",
+ "title": "Checked user permissions",
+ "status": "pending",
+ },
+ {
+ "task_id": "call_003",
+ "title": "Generated comprehensive user report",
+ "status": "complete",
+ "output": {
+ "type": "rich_text",
+ "elements": [
+ {"type": "rich_text_section", "elements": [{"type": "text", "text": "15 data points compiled"}]}
+ ],
+ },
+ },
+ ],
+ }
+ self.assertDictEqual(input, PlanBlock(**input).to_dict())
+ self.assertDictEqual(input, Block.parse(input).to_dict())
+
+
+# ----------------------------------------------
+# Task card
+# ----------------------------------------------
+
+
+class TaskCardBlockTests(unittest.TestCase):
+ def test_document(self):
+ input = {
+ "type": "task_card",
+ "task_id": "task_1",
+ "title": "Fetching weather data",
+ "status": "pending",
+ "output": {
+ "type": "rich_text",
+ "elements": [
+ {
+ "type": "rich_text_section",
+ "elements": [{"type": "text", "text": "Found weather data for Chicago from 2 sources"}],
+ }
+ ],
+ },
+ "sources": [
+ {"type": "url", "url": "https://weather.com/", "text": "weather.com"},
+ {"type": "url", "url": "https://www.accuweather.com/", "text": "accuweather.com"},
+ ],
+ }
+ self.assertDictEqual(input, TaskCardBlock(**input).to_dict())
+ self.assertDictEqual(input, Block.parse(input).to_dict())
+
+
# ----------------------------------------------
# Video
# ----------------------------------------------
diff --git a/tests/slack_sdk/models/test_chunks.py b/tests/slack_sdk/models/test_chunks.py
index 1b8b58c96..202b4bf77 100644
--- a/tests/slack_sdk/models/test_chunks.py
+++ b/tests/slack_sdk/models/test_chunks.py
@@ -1,6 +1,7 @@
import unittest
-from slack_sdk.models.messages.chunk import MarkdownTextChunk, TaskUpdateChunk, URLSource
+from slack_sdk.models.blocks.block_elements import UrlSourceElement
+from slack_sdk.models.messages.chunk import MarkdownTextChunk, TaskUpdateChunk
class MarkdownTextChunkTests(unittest.TestCase):
@@ -47,7 +48,11 @@ def test_json(self):
status="complete",
output="Found a solution",
sources=[
- URLSource(
+ UrlSourceElement(
+ text="Discussion of Life's Questions",
+ url="https://www.answers.com",
+ ),
+ UrlSourceElement(
text="The Free Encyclopedia",
url="https://wikipedia.org",
icon_url="https://example.com/globe.png",
@@ -61,6 +66,11 @@ def test_json(self):
"status": "complete",
"output": "Found a solution",
"sources": [
+ {
+ "type": "url",
+ "text": "Discussion of Life's Questions",
+ "url": "https://www.answers.com",
+ },
{
"type": "url",
"text": "The Free Encyclopedia",
From b1dc1990f6cad35d87d1ccf5e0eec2f502a0ad90 Mon Sep 17 00:00:00 2001
From: Eden Zimbelman
Date: Fri, 16 Jan 2026 16:03:59 -0800
Subject: [PATCH 11/13] feat: add task_display_mode option to the start of chat
streams (#1820)
Co-authored-by: Michael Brooks
---
slack_sdk/web/async_chat_stream.py | 4 ++++
slack_sdk/web/async_client.py | 6 ++++++
slack_sdk/web/chat_stream.py | 4 ++++
slack_sdk/web/client.py | 6 ++++++
slack_sdk/web/legacy_client.py | 2 ++
tests/slack_sdk/web/test_chat_stream.py | 2 ++
6 files changed, 24 insertions(+)
diff --git a/slack_sdk/web/async_chat_stream.py b/slack_sdk/web/async_chat_stream.py
index 550902182..7af774bbc 100644
--- a/slack_sdk/web/async_chat_stream.py
+++ b/slack_sdk/web/async_chat_stream.py
@@ -39,6 +39,7 @@ def __init__(
buffer_size: int,
recipient_team_id: Optional[str] = None,
recipient_user_id: Optional[str] = None,
+ task_display_mode: Optional[str] = None,
**kwargs,
):
"""Initialize a new ChatStream instance.
@@ -54,6 +55,8 @@ def __init__(
recipient_team_id: The encoded ID of the team the user receiving the streaming text belongs to. Required when
streaming to channels.
recipient_user_id: The encoded ID of the user to receive the streaming text. Required when streaming to channels.
+ task_display_mode: Specifies how tasks are displayed in the message. A "timeline" displays individual tasks
+ interleaved with text and "plan" displays all tasks together.
buffer_size: The length of markdown_text to buffer in-memory before calling a method. Increasing this value
decreases the number of method calls made for the same amount of text, which is useful to avoid rate limits.
**kwargs: Additional arguments passed to the underlying API calls.
@@ -66,6 +69,7 @@ def __init__(
"thread_ts": thread_ts,
"recipient_team_id": recipient_team_id,
"recipient_user_id": recipient_user_id,
+ "task_display_mode": task_display_mode,
**kwargs,
}
self._buffer = ""
diff --git a/slack_sdk/web/async_client.py b/slack_sdk/web/async_client.py
index 5aaa2c610..7464b754d 100644
--- a/slack_sdk/web/async_client.py
+++ b/slack_sdk/web/async_client.py
@@ -2889,6 +2889,7 @@ async def chat_startStream(
recipient_team_id: Optional[str] = None,
recipient_user_id: Optional[str] = None,
chunks: Optional[Sequence[Union[Dict, Chunk]]] = None,
+ task_display_mode: Optional[str] = None, # timeline, plan
**kwargs,
) -> AsyncSlackResponse:
"""Starts a new streaming conversation.
@@ -2902,6 +2903,7 @@ async def chat_startStream(
"recipient_team_id": recipient_team_id,
"recipient_user_id": recipient_user_id,
"chunks": chunks,
+ "task_display_mode": task_display_mode,
}
)
_parse_web_class_objects(kwargs)
@@ -2944,6 +2946,7 @@ async def chat_stream(
thread_ts: str,
recipient_team_id: Optional[str] = None,
recipient_user_id: Optional[str] = None,
+ task_display_mode: Optional[str] = None,
**kwargs,
) -> AsyncChatStream:
"""Stream markdown text into a conversation.
@@ -2970,6 +2973,8 @@ async def chat_stream(
recipient_team_id: The encoded ID of the team the user receiving the streaming text belongs to. Required when
streaming to channels.
recipient_user_id: The encoded ID of the user to receive the streaming text. Required when streaming to channels.
+ task_display_mode: Specifies how tasks are displayed in the message. A "timeline" displays individual tasks
+ interleaved with text and "plan" displays all tasks together.
**kwargs: Additional arguments passed to the underlying API calls.
Returns:
@@ -2995,6 +3000,7 @@ async def chat_stream(
thread_ts=thread_ts,
recipient_team_id=recipient_team_id,
recipient_user_id=recipient_user_id,
+ task_display_mode=task_display_mode,
buffer_size=buffer_size,
**kwargs,
)
diff --git a/slack_sdk/web/chat_stream.py b/slack_sdk/web/chat_stream.py
index acdac728a..580f7cacb 100644
--- a/slack_sdk/web/chat_stream.py
+++ b/slack_sdk/web/chat_stream.py
@@ -29,6 +29,7 @@ def __init__(
buffer_size: int,
recipient_team_id: Optional[str] = None,
recipient_user_id: Optional[str] = None,
+ task_display_mode: Optional[str] = None,
**kwargs,
):
"""Initialize a new ChatStream instance.
@@ -44,6 +45,8 @@ def __init__(
recipient_team_id: The encoded ID of the team the user receiving the streaming text belongs to. Required when
streaming to channels.
recipient_user_id: The encoded ID of the user to receive the streaming text. Required when streaming to channels.
+ task_display_mode: Specifies how tasks are displayed in the message. A "timeline" displays individual tasks
+ interleaved with text and "plan" displays all tasks together.
buffer_size: The length of markdown_text to buffer in-memory before calling a method. Increasing this value
decreases the number of method calls made for the same amount of text, which is useful to avoid rate limits.
**kwargs: Additional arguments passed to the underlying API calls.
@@ -56,6 +59,7 @@ def __init__(
"thread_ts": thread_ts,
"recipient_team_id": recipient_team_id,
"recipient_user_id": recipient_user_id,
+ "task_display_mode": task_display_mode,
**kwargs,
}
self._buffer = ""
diff --git a/slack_sdk/web/client.py b/slack_sdk/web/client.py
index 392a261ad..638b4ab67 100644
--- a/slack_sdk/web/client.py
+++ b/slack_sdk/web/client.py
@@ -2879,6 +2879,7 @@ def chat_startStream(
recipient_team_id: Optional[str] = None,
recipient_user_id: Optional[str] = None,
chunks: Optional[Sequence[Union[Dict, Chunk]]] = None,
+ task_display_mode: Optional[str] = None, # timeline, plan
**kwargs,
) -> SlackResponse:
"""Starts a new streaming conversation.
@@ -2892,6 +2893,7 @@ def chat_startStream(
"recipient_team_id": recipient_team_id,
"recipient_user_id": recipient_user_id,
"chunks": chunks,
+ "task_display_mode": task_display_mode,
}
)
_parse_web_class_objects(kwargs)
@@ -2934,6 +2936,7 @@ def chat_stream(
thread_ts: str,
recipient_team_id: Optional[str] = None,
recipient_user_id: Optional[str] = None,
+ task_display_mode: Optional[str] = None,
**kwargs,
) -> ChatStream:
"""Stream markdown text into a conversation.
@@ -2960,6 +2963,8 @@ def chat_stream(
recipient_team_id: The encoded ID of the team the user receiving the streaming text belongs to. Required when
streaming to channels.
recipient_user_id: The encoded ID of the user to receive the streaming text. Required when streaming to channels.
+ task_display_mode: Specifies how tasks are displayed in the message. A "timeline" displays individual tasks
+ interleaved with text and "plan" displays all tasks together.
**kwargs: Additional arguments passed to the underlying API calls.
Returns:
@@ -2985,6 +2990,7 @@ def chat_stream(
thread_ts=thread_ts,
recipient_team_id=recipient_team_id,
recipient_user_id=recipient_user_id,
+ task_display_mode=task_display_mode,
buffer_size=buffer_size,
**kwargs,
)
diff --git a/slack_sdk/web/legacy_client.py b/slack_sdk/web/legacy_client.py
index 7bb0609c5..061be7c85 100644
--- a/slack_sdk/web/legacy_client.py
+++ b/slack_sdk/web/legacy_client.py
@@ -2890,6 +2890,7 @@ def chat_startStream(
recipient_team_id: Optional[str] = None,
recipient_user_id: Optional[str] = None,
chunks: Optional[Sequence[Union[Dict, Chunk]]] = None,
+ task_display_mode: Optional[str] = None, # timeline, plan
**kwargs,
) -> Union[Future, SlackResponse]:
"""Starts a new streaming conversation.
@@ -2903,6 +2904,7 @@ def chat_startStream(
"recipient_team_id": recipient_team_id,
"recipient_user_id": recipient_user_id,
"chunks": chunks,
+ "task_display_mode": task_display_mode,
}
)
_parse_web_class_objects(kwargs)
diff --git a/tests/slack_sdk/web/test_chat_stream.py b/tests/slack_sdk/web/test_chat_stream.py
index a6d846769..0a11b9d53 100644
--- a/tests/slack_sdk/web/test_chat_stream.py
+++ b/tests/slack_sdk/web/test_chat_stream.py
@@ -185,6 +185,7 @@ def test_streams_a_chunk_message(self):
recipient_team_id="T0123456789",
recipient_user_id="U0123456789",
thread_ts="123.000",
+ task_display_mode="timeline",
)
streamer.append(markdown_text="**this is ")
streamer.append(markdown_text="buffered**")
@@ -223,6 +224,7 @@ def test_streams_a_chunk_message(self):
)
self.assertEqual(start_request.get("recipient_team_id"), "T0123456789")
self.assertEqual(start_request.get("recipient_user_id"), "U0123456789")
+ self.assertEqual(start_request.get("task_display_mode"), "timeline")
append_request = self.thread.server.chat_stream_requests.get("/chat.appendStream", {})
self.assertEqual(append_request.get("channel"), "C0123456789")
From 70327cad5c0121dfc28f0a403a1c0d09d11f215a Mon Sep 17 00:00:00 2001
From: Eden Zimbelman
Date: Fri, 16 Jan 2026 16:28:04 -0800
Subject: [PATCH 12/13] feat: add plan_update chunk for chat streaming (#1821)
Co-authored-by: Michael Brooks
---
slack_sdk/models/messages/chunk.py | 25 ++++++++++++++++++++++
tests/slack_sdk/models/test_chunks.py | 13 ++++++++++-
tests/slack_sdk/web/test_internal_utils.py | 16 ++++++++++----
3 files changed, 49 insertions(+), 5 deletions(-)
diff --git a/slack_sdk/models/messages/chunk.py b/slack_sdk/models/messages/chunk.py
index b00daebd1..4db35621b 100644
--- a/slack_sdk/models/messages/chunk.py
+++ b/slack_sdk/models/messages/chunk.py
@@ -34,6 +34,8 @@ def parse(cls, chunk: Union[Dict, "Chunk"]) -> Optional["Chunk"]:
type = chunk["type"]
if type == MarkdownTextChunk.type:
return MarkdownTextChunk(**chunk)
+ elif type == PlanUpdateChunk.type:
+ return PlanUpdateChunk(**chunk)
elif type == TaskUpdateChunk.type:
return TaskUpdateChunk(**chunk)
else:
@@ -67,6 +69,29 @@ def __init__(
self.text = text
+class PlanUpdateChunk(Chunk):
+ type = "plan_update"
+
+ @property
+ def attributes(self) -> Set[str]: # type: ignore[override]
+ return super().attributes.union({"title"})
+
+ def __init__(
+ self,
+ *,
+ title: str,
+ **others: Dict,
+ ):
+ """An updated title of plans for task and tool calls.
+
+ https://docs.slack.dev/messaging/sending-and-scheduling-messages#text-streaming
+ """
+ super().__init__(type=self.type)
+ show_unknown_key_warning(self, others)
+
+ self.title = title
+
+
class TaskUpdateChunk(Chunk):
type = "task_update"
diff --git a/tests/slack_sdk/models/test_chunks.py b/tests/slack_sdk/models/test_chunks.py
index 202b4bf77..293f5acb2 100644
--- a/tests/slack_sdk/models/test_chunks.py
+++ b/tests/slack_sdk/models/test_chunks.py
@@ -1,7 +1,7 @@
import unittest
from slack_sdk.models.blocks.block_elements import UrlSourceElement
-from slack_sdk.models.messages.chunk import MarkdownTextChunk, TaskUpdateChunk
+from slack_sdk.models.messages.chunk import MarkdownTextChunk, PlanUpdateChunk, TaskUpdateChunk
class MarkdownTextChunkTests(unittest.TestCase):
@@ -15,6 +15,17 @@ def test_json(self):
)
+class PlanUpdateChunkTests(unittest.TestCase):
+ def test_json(self):
+ self.assertDictEqual(
+ PlanUpdateChunk(title="Crunching numbers...").to_dict(),
+ {
+ "type": "plan_update",
+ "title": "Crunching numbers...",
+ },
+ )
+
+
class TaskUpdateChunkTests(unittest.TestCase):
def test_json(self):
self.assertDictEqual(
diff --git a/tests/slack_sdk/web/test_internal_utils.py b/tests/slack_sdk/web/test_internal_utils.py
index fc6574aab..3e44f0c9c 100644
--- a/tests/slack_sdk/web/test_internal_utils.py
+++ b/tests/slack_sdk/web/test_internal_utils.py
@@ -4,10 +4,9 @@
from pathlib import Path
from typing import Dict
-
from slack_sdk.models.attachments import Attachment
from slack_sdk.models.blocks import Block, DividerBlock
-from slack_sdk.models.messages.chunk import MarkdownTextChunk, TaskUpdateChunk
+from slack_sdk.models.messages.chunk import MarkdownTextChunk, PlanUpdateChunk, TaskUpdateChunk
from slack_sdk.web.internal_utils import (
_build_unexpected_body_error_message,
_get_url,
@@ -59,9 +58,14 @@ def test_can_parse_sequence_of_attachments(self):
def test_can_parse_sequence_of_chunks(self):
for chunks in [
- [MarkdownTextChunk(text="fiz"), TaskUpdateChunk(id="001", title="baz", status="complete")], # list
+ [
+ MarkdownTextChunk(text="fiz"),
+ PlanUpdateChunk(title="fuz"),
+ TaskUpdateChunk(id="001", title="baz", status="complete"),
+ ], # list
(
MarkdownTextChunk(text="fiz"),
+ PlanUpdateChunk(title="fuz"),
TaskUpdateChunk(id="001", title="baz", status="complete"),
), # tuple
]:
@@ -87,7 +91,11 @@ def test_can_parse_str_attachments(self):
def test_can_parse_str_chunks(self):
input = json.dumps(
- [MarkdownTextChunk(text="fiz").to_dict(), TaskUpdateChunk(id="001", title="baz", status="complete").to_dict()]
+ [
+ MarkdownTextChunk(text="fiz").to_dict(),
+ PlanUpdateChunk(title="fuz").to_dict(),
+ TaskUpdateChunk(id="001", title="baz", status="complete").to_dict(),
+ ]
)
kwargs = {"chunks": input}
_parse_web_class_objects(kwargs)
From 610de5b0c649f929933546415f35f9a56558e83b Mon Sep 17 00:00:00 2001
From: Michael Brooks
Date: Fri, 16 Jan 2026 16:33:33 -0800
Subject: [PATCH 13/13] chore(release): version 3.40.0.dev0
---
docs/reference/index.html | 43 +-
docs/reference/models/basic_objects.html | 1 +
.../models/blocks/block_elements.html | 112 +++++
docs/reference/models/blocks/blocks.html | 279 +++++++++++
docs/reference/models/blocks/index.html | 391 +++++++++++++++
docs/reference/models/index.html | 1 +
docs/reference/models/messages/chunk.html | 447 ++++++++++++++++++
docs/reference/models/messages/index.html | 5 +
.../oauth/installation_store/index.html | 16 +-
.../oauth/installation_store/models/bot.html | 6 +-
.../installation_store/models/index.html | 16 +-
.../models/installation.html | 10 +-
.../oauth/state_store/sqlalchemy/index.html | 16 +-
docs/reference/web/async_chat_stream.html | 70 ++-
docs/reference/web/async_client.html | 43 +-
docs/reference/web/chat_stream.html | 70 ++-
docs/reference/web/client.html | 43 +-
docs/reference/web/index.html | 43 +-
docs/reference/web/legacy_client.html | 30 +-
slack_sdk/version.py | 2 +-
20 files changed, 1557 insertions(+), 87 deletions(-)
create mode 100644 docs/reference/models/messages/chunk.html
diff --git a/docs/reference/index.html b/docs/reference/index.html
index 7cc7723c1..c2f518bcf 100644
--- a/docs/reference/index.html
+++ b/docs/reference/index.html
@@ -2755,7 +2755,8 @@
*,
channel: str,
ts: str,
- markdown_text: str,
+ markdown_text: Optional[str] = None,
+ chunks: Optional[Sequence[Union[Dict, Chunk]]] = None,
**kwargs,
) -> SlackResponse:
"""Appends text to an existing streaming conversation.
@@ -2766,8 +2767,10 @@
"channel": channel,
"ts": ts,
"markdown_text": markdown_text,
+ "chunks": chunks,
}
)
+ _parse_web_class_objects(kwargs)
kwargs = _remove_none_values(kwargs)
return self.api_call("chat.appendStream", json=kwargs)
@@ -3009,6 +3012,8 @@
markdown_text: Optional[str] = None,
recipient_team_id: Optional[str] = None,
recipient_user_id: Optional[str] = None,
+ chunks: Optional[Sequence[Union[Dict, Chunk]]] = None,
+ task_display_mode: Optional[str] = None, # timeline, plan
**kwargs,
) -> SlackResponse:
"""Starts a new streaming conversation.
@@ -3021,8 +3026,11 @@
"markdown_text": markdown_text,
"recipient_team_id": recipient_team_id,
"recipient_user_id": recipient_user_id,
+ "chunks": chunks,
+ "task_display_mode": task_display_mode,
}
)
+ _parse_web_class_objects(kwargs)
kwargs = _remove_none_values(kwargs)
return self.api_call("chat.startStream", json=kwargs)
@@ -3034,6 +3042,7 @@
markdown_text: Optional[str] = None,
blocks: Optional[Union[str, Sequence[Union[Dict, Block]]]] = None,
metadata: Optional[Union[Dict, Metadata]] = None,
+ chunks: Optional[Sequence[Union[Dict, Chunk]]] = None,
**kwargs,
) -> SlackResponse:
"""Stops a streaming conversation.
@@ -3046,6 +3055,7 @@
"markdown_text": markdown_text,
"blocks": blocks,
"metadata": metadata,
+ "chunks": chunks,
}
)
_parse_web_class_objects(kwargs)
@@ -3060,6 +3070,7 @@
thread_ts: str,
recipient_team_id: Optional[str] = None,
recipient_user_id: Optional[str] = None,
+ task_display_mode: Optional[str] = None,
**kwargs,
) -> ChatStream:
"""Stream markdown text into a conversation.
@@ -3086,6 +3097,8 @@
recipient_team_id: The encoded ID of the team the user receiving the streaming text belongs to. Required when
streaming to channels.
recipient_user_id: The encoded ID of the user to receive the streaming text. Required when streaming to channels.
+ task_display_mode: Specifies how tasks are displayed in the message. A "timeline" displays individual tasks
+ interleaved with text and "plan" displays all tasks together.
**kwargs: Additional arguments passed to the underlying API calls.
Returns:
@@ -3111,6 +3124,7 @@
thread_ts=thread_ts,
recipient_team_id=recipient_team_id,
recipient_user_id=recipient_user_id,
+ task_display_mode=task_display_mode,
buffer_size=buffer_size,
**kwargs,
)
@@ -10248,7 +10262,7 @@ Methods
-def chat_appendStream(self, *, channel: str, ts: str, markdown_text: str, **kwargs) ‑> SlackResponse
+def chat_appendStream(self,
*,
channel: str,
ts: str,
markdown_text: str | None = None,
chunks: Sequence[Dict | Chunk] | None = None,
**kwargs) ‑> SlackResponse
@@ -10260,7 +10274,8 @@ Methods
*,
channel: str,
ts: str,
- markdown_text: str,
+ markdown_text: Optional[str] = None,
+ chunks: Optional[Sequence[Union[Dict, Chunk]]] = None,
**kwargs,
) -> SlackResponse:
"""Appends text to an existing streaming conversation.
@@ -10271,8 +10286,10 @@ Methods
"channel": channel,
"ts": ts,
"markdown_text": markdown_text,
+ "chunks": chunks,
}
)
+ _parse_web_class_objects(kwargs)
kwargs = _remove_none_values(kwargs)
return self.api_call("chat.appendStream", json=kwargs)
@@ -10598,7 +10615,7 @@ Methods
https://docs.slack.dev/reference/methods/chat.scheduledMessages.list
-def chat_startStream(self,
*,
channel: str,
thread_ts: str,
markdown_text: str | None = None,
recipient_team_id: str | None = None,
recipient_user_id: str | None = None,
**kwargs) ‑> SlackResponse
+def chat_startStream(self,
*,
channel: str,
thread_ts: str,
markdown_text: str | None = None,
recipient_team_id: str | None = None,
recipient_user_id: str | None = None,
chunks: Sequence[Dict | Chunk] | None = None,
task_display_mode: str | None = None,
**kwargs) ‑> SlackResponse
@@ -10613,6 +10630,8 @@ Methods
markdown_text: Optional[str] = None,
recipient_team_id: Optional[str] = None,
recipient_user_id: Optional[str] = None,
+ chunks: Optional[Sequence[Union[Dict, Chunk]]] = None,
+ task_display_mode: Optional[str] = None, # timeline, plan
**kwargs,
) -> SlackResponse:
"""Starts a new streaming conversation.
@@ -10625,8 +10644,11 @@ Methods
"markdown_text": markdown_text,
"recipient_team_id": recipient_team_id,
"recipient_user_id": recipient_user_id,
+ "chunks": chunks,
+ "task_display_mode": task_display_mode,
}
)
+ _parse_web_class_objects(kwargs)
kwargs = _remove_none_values(kwargs)
return self.api_call("chat.startStream", json=kwargs)
@@ -10634,7 +10656,7 @@ Methods
https://docs.slack.dev/reference/methods/chat.startStream
-def chat_stopStream(self,
*,
channel: str,
ts: str,
markdown_text: str | None = None,
blocks: str | Sequence[Dict | Block] | None = None,
metadata: Dict | Metadata | None = None,
**kwargs) ‑> SlackResponse
+def chat_stopStream(self,
*,
channel: str,
ts: str,
markdown_text: str | None = None,
blocks: str | Sequence[Dict | Block] | None = None,
metadata: Dict | Metadata | None = None,
chunks: Sequence[Dict | Chunk] | None = None,
**kwargs) ‑> SlackResponse
@@ -10649,6 +10671,7 @@ Methods
markdown_text: Optional[str] = None,
blocks: Optional[Union[str, Sequence[Union[Dict, Block]]]] = None,
metadata: Optional[Union[Dict, Metadata]] = None,
+ chunks: Optional[Sequence[Union[Dict, Chunk]]] = None,
**kwargs,
) -> SlackResponse:
"""Stops a streaming conversation.
@@ -10661,6 +10684,7 @@ Methods
"markdown_text": markdown_text,
"blocks": blocks,
"metadata": metadata,
+ "chunks": chunks,
}
)
_parse_web_class_objects(kwargs)
@@ -10671,7 +10695,7 @@ Methods
https://docs.slack.dev/reference/methods/chat.stopStream
-def chat_stream(self,
*,
buffer_size: int = 256,
channel: str,
thread_ts: str,
recipient_team_id: str | None = None,
recipient_user_id: str | None = None,
**kwargs) ‑> ChatStream
+def chat_stream(self,
*,
buffer_size: int = 256,
channel: str,
thread_ts: str,
recipient_team_id: str | None = None,
recipient_user_id: str | None = None,
task_display_mode: str | None = None,
**kwargs) ‑> ChatStream
@@ -10686,6 +10710,7 @@ Methods
thread_ts: str,
recipient_team_id: Optional[str] = None,
recipient_user_id: Optional[str] = None,
+ task_display_mode: Optional[str] = None,
**kwargs,
) -> ChatStream:
"""Stream markdown text into a conversation.
@@ -10712,6 +10737,8 @@ Methods
recipient_team_id: The encoded ID of the team the user receiving the streaming text belongs to. Required when
streaming to channels.
recipient_user_id: The encoded ID of the user to receive the streaming text. Required when streaming to channels.
+ task_display_mode: Specifies how tasks are displayed in the message. A "timeline" displays individual tasks
+ interleaved with text and "plan" displays all tasks together.
**kwargs: Additional arguments passed to the underlying API calls.
Returns:
@@ -10737,6 +10764,7 @@ Methods
thread_ts=thread_ts,
recipient_team_id=recipient_team_id,
recipient_user_id=recipient_user_id,
+ task_display_mode=task_display_mode,
buffer_size=buffer_size,
**kwargs,
)
@@ -10769,6 +10797,9 @@ Args
streaming to channels.
recipient_user_id
The encoded ID of the user to receive the streaming text. Required when streaming to channels.
+task_display_mode
+Specifies how tasks are displayed in the message. A "timeline" displays individual tasks
+interleaved with text and "plan" displays all tasks together.
**kwargs
Additional arguments passed to the underlying API calls.
diff --git a/docs/reference/models/basic_objects.html b/docs/reference/models/basic_objects.html
index 193200f84..cda8d9c95 100644
--- a/docs/reference/models/basic_objects.html
+++ b/docs/reference/models/basic_objects.html
@@ -212,6 +212,7 @@ Subclasses
AbstractDialogSelector
DialogBuilder
DialogTextComponent
+Chunk
Message
ContentItemEntityFields
EntityActionButton
diff --git a/docs/reference/models/blocks/block_elements.html b/docs/reference/models/blocks/block_elements.html
index 375ee07ce..c7f1347fe 100644
--- a/docs/reference/models/blocks/block_elements.html
+++ b/docs/reference/models/blocks/block_elements.html
@@ -130,6 +130,7 @@ Subclasses
ImageElement
InteractiveElement
RichTextElement
+UrlSourceElement
Class variables
@@ -4663,6 +4664,110 @@ Inherited members
+
+class UrlSourceElement
+(*, url: str, text: str, icon_url: str | None = None, **others: Dict)
+
+-
+
+
+Expand source code
+
+class UrlSourceElement(BlockElement):
+ type = "url"
+
+ @property
+ def attributes(self) -> Set[str]: # type: ignore[override]
+ return super().attributes.union(
+ {
+ "url",
+ "text",
+ "icon_url",
+ }
+ )
+
+ def __init__(
+ self,
+ *,
+ url: str,
+ text: str,
+ icon_url: Optional[str] = None,
+ **others: Dict,
+ ):
+ """
+ A URL source element to reference in a task card block.
+ https://docs.slack.dev/reference/block-kit/block-elements/url-source-element
+
+ Args:
+ url (required): The URL type source.
+ text (required): Display text for the URL.
+ icon_url: Optional icon URL to display with the source.
+ """
+ super().__init__(type=self.type)
+ show_unknown_key_warning(self, others)
+ self.url = url
+ self.text = text
+ self.icon_url = icon_url
+
+
+Ancestors
+
+Class variables
+
+var type
+-
+
The type of the None singleton.
+
+
+Instance variables
+
+prop attributes : Set[str]
+-
+
+
+Expand source code
+
+@property
+def attributes(self) -> Set[str]: # type: ignore[override]
+ return super().attributes.union(
+ {
+ "url",
+ "text",
+ "icon_url",
+ }
+ )
+
+Build an unordered collection of unique elements.
+
+
+Inherited members
+
+
class UserMultiSelectElement
(*,
action_id: str | None = None,
placeholder: str | dict | TextObject | None = None,
initial_users: Sequence[str] | None = None,
confirm: dict | ConfirmObject | None = None,
max_selected_items: int | None = None,
focus_on_load: bool | None = None,
**others: dict)
@@ -5319,6 +5424,13 @@
@@ -10665,6 +10693,9 @@ Args
streaming to channels.
recipient_user_id
- The encoded ID of the user to receive the streaming text. Required when streaming to channels.
+task_display_mode
+- Specifies how tasks are displayed in the message. A "timeline" displays individual tasks
+interleaved with text and "plan" displays all tasks together.
**kwargs
- Additional arguments passed to the underlying API calls.
diff --git a/docs/reference/web/index.html b/docs/reference/web/index.html
index 611a26b3b..5780f1f91 100644
--- a/docs/reference/web/index.html
+++ b/docs/reference/web/index.html
@@ -3020,7 +3020,8 @@ Raises
*,
channel: str,
ts: str,
- markdown_text: str,
+ markdown_text: Optional[str] = None,
+ chunks: Optional[Sequence[Union[Dict, Chunk]]] = None,
**kwargs,
) -> SlackResponse:
"""Appends text to an existing streaming conversation.
@@ -3031,8 +3032,10 @@ Raises
"channel": channel,
"ts": ts,
"markdown_text": markdown_text,
+ "chunks": chunks,
}
)
+ _parse_web_class_objects(kwargs)
kwargs = _remove_none_values(kwargs)
return self.api_call("chat.appendStream", json=kwargs)
@@ -3274,6 +3277,8 @@ Raises
markdown_text: Optional[str] = None,
recipient_team_id: Optional[str] = None,
recipient_user_id: Optional[str] = None,
+ chunks: Optional[Sequence[Union[Dict, Chunk]]] = None,
+ task_display_mode: Optional[str] = None, # timeline, plan
**kwargs,
) -> SlackResponse:
"""Starts a new streaming conversation.
@@ -3286,8 +3291,11 @@ Raises
"markdown_text": markdown_text,
"recipient_team_id": recipient_team_id,
"recipient_user_id": recipient_user_id,
+ "chunks": chunks,
+ "task_display_mode": task_display_mode,
}
)
+ _parse_web_class_objects(kwargs)
kwargs = _remove_none_values(kwargs)
return self.api_call("chat.startStream", json=kwargs)
@@ -3299,6 +3307,7 @@ Raises
markdown_text: Optional[str] = None,
blocks: Optional[Union[str, Sequence[Union[Dict, Block]]]] = None,
metadata: Optional[Union[Dict, Metadata]] = None,
+ chunks: Optional[Sequence[Union[Dict, Chunk]]] = None,
**kwargs,
) -> SlackResponse:
"""Stops a streaming conversation.
@@ -3311,6 +3320,7 @@ Raises
"markdown_text": markdown_text,
"blocks": blocks,
"metadata": metadata,
+ "chunks": chunks,
}
)
_parse_web_class_objects(kwargs)
@@ -3325,6 +3335,7 @@ Raises
thread_ts: str,
recipient_team_id: Optional[str] = None,
recipient_user_id: Optional[str] = None,
+ task_display_mode: Optional[str] = None,
**kwargs,
) -> ChatStream:
"""Stream markdown text into a conversation.
@@ -3351,6 +3362,8 @@ Raises
recipient_team_id: The encoded ID of the team the user receiving the streaming text belongs to. Required when
streaming to channels.
recipient_user_id: The encoded ID of the user to receive the streaming text. Required when streaming to channels.
+ task_display_mode: Specifies how tasks are displayed in the message. A "timeline" displays individual tasks
+ interleaved with text and "plan" displays all tasks together.
**kwargs: Additional arguments passed to the underlying API calls.
Returns:
@@ -3376,6 +3389,7 @@ Raises
thread_ts=thread_ts,
recipient_team_id=recipient_team_id,
recipient_user_id=recipient_user_id,
+ task_display_mode=task_display_mode,
buffer_size=buffer_size,
**kwargs,
)
@@ -10513,7 +10527,7 @@ Methods
-def chat_appendStream(self, *, channel: str, ts: str, markdown_text: str, **kwargs) ‑> SlackResponse
+def chat_appendStream(self,
*,
channel: str,
ts: str,
markdown_text: str | None = None,
chunks: Sequence[Dict | Chunk] | None = None,
**kwargs) ‑> SlackResponse
@@ -10525,7 +10539,8 @@ Methods
*,
channel: str,
ts: str,
- markdown_text: str,
+ markdown_text: Optional[str] = None,
+ chunks: Optional[Sequence[Union[Dict, Chunk]]] = None,
**kwargs,
) -> SlackResponse:
"""Appends text to an existing streaming conversation.
@@ -10536,8 +10551,10 @@ Methods
"channel": channel,
"ts": ts,
"markdown_text": markdown_text,
+ "chunks": chunks,
}
)
+ _parse_web_class_objects(kwargs)
kwargs = _remove_none_values(kwargs)
return self.api_call("chat.appendStream", json=kwargs)
@@ -10863,7 +10880,7 @@ Methods
https://docs.slack.dev/reference/methods/chat.scheduledMessages.list
-def chat_startStream(self,
*,
channel: str,
thread_ts: str,
markdown_text: str | None = None,
recipient_team_id: str | None = None,
recipient_user_id: str | None = None,
**kwargs) ‑> SlackResponse
+def chat_startStream(self,
*,
channel: str,
thread_ts: str,
markdown_text: str | None = None,
recipient_team_id: str | None = None,
recipient_user_id: str | None = None,
chunks: Sequence[Dict | Chunk] | None = None,
task_display_mode: str | None = None,
**kwargs) ‑> SlackResponse
@@ -10878,6 +10895,8 @@ Methods
markdown_text: Optional[str] = None,
recipient_team_id: Optional[str] = None,
recipient_user_id: Optional[str] = None,
+ chunks: Optional[Sequence[Union[Dict, Chunk]]] = None,
+ task_display_mode: Optional[str] = None, # timeline, plan
**kwargs,
) -> SlackResponse:
"""Starts a new streaming conversation.
@@ -10890,8 +10909,11 @@ Methods
"markdown_text": markdown_text,
"recipient_team_id": recipient_team_id,
"recipient_user_id": recipient_user_id,
+ "chunks": chunks,
+ "task_display_mode": task_display_mode,
}
)
+ _parse_web_class_objects(kwargs)
kwargs = _remove_none_values(kwargs)
return self.api_call("chat.startStream", json=kwargs)
@@ -10899,7 +10921,7 @@ Methods
https://docs.slack.dev/reference/methods/chat.startStream
-def chat_stopStream(self,
*,
channel: str,
ts: str,
markdown_text: str | None = None,
blocks: str | Sequence[Dict | Block] | None = None,
metadata: Dict | Metadata | None = None,
**kwargs) ‑> SlackResponse
+def chat_stopStream(self,
*,
channel: str,
ts: str,
markdown_text: str | None = None,
blocks: str | Sequence[Dict | Block] | None = None,
metadata: Dict | Metadata | None = None,
chunks: Sequence[Dict | Chunk] | None = None,
**kwargs) ‑> SlackResponse
@@ -10914,6 +10936,7 @@ Methods
markdown_text: Optional[str] = None,
blocks: Optional[Union[str, Sequence[Union[Dict, Block]]]] = None,
metadata: Optional[Union[Dict, Metadata]] = None,
+ chunks: Optional[Sequence[Union[Dict, Chunk]]] = None,
**kwargs,
) -> SlackResponse:
"""Stops a streaming conversation.
@@ -10926,6 +10949,7 @@ Methods
"markdown_text": markdown_text,
"blocks": blocks,
"metadata": metadata,
+ "chunks": chunks,
}
)
_parse_web_class_objects(kwargs)
@@ -10936,7 +10960,7 @@ Methods
https://docs.slack.dev/reference/methods/chat.stopStream
-def chat_stream(self,
*,
buffer_size: int = 256,
channel: str,
thread_ts: str,
recipient_team_id: str | None = None,
recipient_user_id: str | None = None,
**kwargs) ‑> ChatStream
+def chat_stream(self,
*,
buffer_size: int = 256,
channel: str,
thread_ts: str,
recipient_team_id: str | None = None,
recipient_user_id: str | None = None,
task_display_mode: str | None = None,
**kwargs) ‑> ChatStream
@@ -10951,6 +10975,7 @@ Methods
thread_ts: str,
recipient_team_id: Optional[str] = None,
recipient_user_id: Optional[str] = None,
+ task_display_mode: Optional[str] = None,
**kwargs,
) -> ChatStream:
"""Stream markdown text into a conversation.
@@ -10977,6 +11002,8 @@ Methods
recipient_team_id: The encoded ID of the team the user receiving the streaming text belongs to. Required when
streaming to channels.
recipient_user_id: The encoded ID of the user to receive the streaming text. Required when streaming to channels.
+ task_display_mode: Specifies how tasks are displayed in the message. A "timeline" displays individual tasks
+ interleaved with text and "plan" displays all tasks together.
**kwargs: Additional arguments passed to the underlying API calls.
Returns:
@@ -11002,6 +11029,7 @@ Methods
thread_ts=thread_ts,
recipient_team_id=recipient_team_id,
recipient_user_id=recipient_user_id,
+ task_display_mode=task_display_mode,
buffer_size=buffer_size,
**kwargs,
)
@@ -11034,6 +11062,9 @@ Args
streaming to channels.
recipient_user_id
The encoded ID of the user to receive the streaming text. Required when streaming to channels.
+task_display_mode
+Specifies how tasks are displayed in the message. A "timeline" displays individual tasks
+interleaved with text and "plan" displays all tasks together.
**kwargs
Additional arguments passed to the underlying API calls.
diff --git a/docs/reference/web/legacy_client.html b/docs/reference/web/legacy_client.html
index d100178dc..86d9b1d0c 100644
--- a/docs/reference/web/legacy_client.html
+++ b/docs/reference/web/legacy_client.html
@@ -2650,7 +2650,8 @@
*,
channel: str,
ts: str,
- markdown_text: str,
+ markdown_text: Optional[str] = None,
+ chunks: Optional[Sequence[Union[Dict, Chunk]]] = None,
**kwargs,
) -> Union[Future, SlackResponse]:
"""Appends text to an existing streaming conversation.
@@ -2661,8 +2662,10 @@
"channel": channel,
"ts": ts,
"markdown_text": markdown_text,
+ "chunks": chunks,
}
)
+ _parse_web_class_objects(kwargs)
kwargs = _remove_none_values(kwargs)
return self.api_call("chat.appendStream", json=kwargs)
@@ -2904,6 +2907,8 @@
markdown_text: Optional[str] = None,
recipient_team_id: Optional[str] = None,
recipient_user_id: Optional[str] = None,
+ chunks: Optional[Sequence[Union[Dict, Chunk]]] = None,
+ task_display_mode: Optional[str] = None, # timeline, plan
**kwargs,
) -> Union[Future, SlackResponse]:
"""Starts a new streaming conversation.
@@ -2916,8 +2921,11 @@
"markdown_text": markdown_text,
"recipient_team_id": recipient_team_id,
"recipient_user_id": recipient_user_id,
+ "chunks": chunks,
+ "task_display_mode": task_display_mode,
}
)
+ _parse_web_class_objects(kwargs)
kwargs = _remove_none_values(kwargs)
return self.api_call("chat.startStream", json=kwargs)
@@ -2929,6 +2937,7 @@
markdown_text: Optional[str] = None,
blocks: Optional[Union[str, Sequence[Union[Dict, Block]]]] = None,
metadata: Optional[Union[Dict, Metadata]] = None,
+ chunks: Optional[Sequence[Union[Dict, Chunk]]] = None,
**kwargs,
) -> Union[Future, SlackResponse]:
"""Stops a streaming conversation.
@@ -2941,6 +2950,7 @@
"markdown_text": markdown_text,
"blocks": blocks,
"metadata": metadata,
+ "chunks": chunks,
}
)
_parse_web_class_objects(kwargs)
@@ -10080,7 +10090,7 @@ Methods
-def chat_appendStream(self, *, channel: str, ts: str, markdown_text: str, **kwargs) ‑> _asyncio.Future | LegacySlackResponse
+def chat_appendStream(self,
*,
channel: str,
ts: str,
markdown_text: str | None = None,
chunks: Sequence[Dict | Chunk] | None = None,
**kwargs) ‑> _asyncio.Future | LegacySlackResponse
@@ -10092,7 +10102,8 @@ Methods
*,
channel: str,
ts: str,
- markdown_text: str,
+ markdown_text: Optional[str] = None,
+ chunks: Optional[Sequence[Union[Dict, Chunk]]] = None,
**kwargs,
) -> Union[Future, SlackResponse]:
"""Appends text to an existing streaming conversation.
@@ -10103,8 +10114,10 @@ Methods
"channel": channel,
"ts": ts,
"markdown_text": markdown_text,
+ "chunks": chunks,
}
)
+ _parse_web_class_objects(kwargs)
kwargs = _remove_none_values(kwargs)
return self.api_call("chat.appendStream", json=kwargs)
@@ -10430,7 +10443,7 @@ Methods
https://docs.slack.dev/reference/methods/chat.scheduledMessages.list
-def chat_startStream(self,
*,
channel: str,
thread_ts: str,
markdown_text: str | None = None,
recipient_team_id: str | None = None,
recipient_user_id: str | None = None,
**kwargs) ‑> _asyncio.Future | LegacySlackResponse
+def chat_startStream(self,
*,
channel: str,
thread_ts: str,
markdown_text: str | None = None,
recipient_team_id: str | None = None,
recipient_user_id: str | None = None,
chunks: Sequence[Dict | Chunk] | None = None,
task_display_mode: str | None = None,
**kwargs) ‑> _asyncio.Future | LegacySlackResponse
@@ -10445,6 +10458,8 @@ Methods
markdown_text: Optional[str] = None,
recipient_team_id: Optional[str] = None,
recipient_user_id: Optional[str] = None,
+ chunks: Optional[Sequence[Union[Dict, Chunk]]] = None,
+ task_display_mode: Optional[str] = None, # timeline, plan
**kwargs,
) -> Union[Future, SlackResponse]:
"""Starts a new streaming conversation.
@@ -10457,8 +10472,11 @@ Methods
"markdown_text": markdown_text,
"recipient_team_id": recipient_team_id,
"recipient_user_id": recipient_user_id,
+ "chunks": chunks,
+ "task_display_mode": task_display_mode,
}
)
+ _parse_web_class_objects(kwargs)
kwargs = _remove_none_values(kwargs)
return self.api_call("chat.startStream", json=kwargs)
@@ -10466,7 +10484,7 @@ Methods
https://docs.slack.dev/reference/methods/chat.startStream
-def chat_stopStream(self,
*,
channel: str,
ts: str,
markdown_text: str | None = None,
blocks: str | Sequence[Dict | Block] | None = None,
metadata: Dict | Metadata | None = None,
**kwargs) ‑> _asyncio.Future | LegacySlackResponse
+def chat_stopStream(self,
*,
channel: str,
ts: str,
markdown_text: str | None = None,
blocks: str | Sequence[Dict | Block] | None = None,
metadata: Dict | Metadata | None = None,
chunks: Sequence[Dict | Chunk] | None = None,
**kwargs) ‑> _asyncio.Future | LegacySlackResponse
@@ -10481,6 +10499,7 @@ Methods
markdown_text: Optional[str] = None,
blocks: Optional[Union[str, Sequence[Union[Dict, Block]]]] = None,
metadata: Optional[Union[Dict, Metadata]] = None,
+ chunks: Optional[Sequence[Union[Dict, Chunk]]] = None,
**kwargs,
) -> Union[Future, SlackResponse]:
"""Stops a streaming conversation.
@@ -10493,6 +10512,7 @@ Methods
"markdown_text": markdown_text,
"blocks": blocks,
"metadata": metadata,
+ "chunks": chunks,
}
)
_parse_web_class_objects(kwargs)
diff --git a/slack_sdk/version.py b/slack_sdk/version.py
index fb572e0ba..7f81a760a 100644
--- a/slack_sdk/version.py
+++ b/slack_sdk/version.py
@@ -1,3 +1,3 @@
"""Check the latest version at https://pypi.org/project/slack-sdk/"""
-__version__ = "3.39.0"
+__version__ = "3.40.0.dev0"