diff --git a/.claude/skills/managing-dependencies/SKILL.md b/.claude/skills/managing-dependencies/SKILL.md
index 8f67fccd8..335ac7d0a 100644
--- a/.claude/skills/managing-dependencies/SKILL.md
+++ b/.claude/skills/managing-dependencies/SKILL.md
@@ -1,127 +1,81 @@
---
name: managing-dependencies
description: >-
- Use when adding, updating, pinning, or reviewing any dependency in this repo's requirements/*.txt files, including taking a Dependabot bump or writing a requirement line by hand. Also use when a pip install fails on the older Python jobs while newer ones pass, or when CI errors with "Could not find a version that satisfies", "No matching distribution found", "Requires-Python >=3.x", or "ResolutionImpossible". Triggers include: "add to requirements", "bump/update ", "pin ", "fix this dependabot PR", "CI can't install on Python 3.7/3.8/3.9", "requires-python error in the install step". This skill defines the layout and version-constraint conventions to follow; reach for it before hand-editing any requirements/*.txt file.
+ Use when adding, bumping, pinning, or reviewing a dependency in requirements.txt or requirements/*.txt, including taking or fixing a Dependabot PR. Also use when pip install fails on some Python versions in CI but not others ("Could not find a version that satisfies the requirement", "No matching distribution found", "Requires-Python", "ResolutionImpossible"), including PyPy-only failures, and when dropping support for an old Python version.
---
# Managing dependencies
-## Overview
+This repo supports a range of Python versions, and one set of requirements files has to install on all of them. Dependabot proposes the newest release, so its bumps often raise a lower bound past what the oldest Pythons can install, and CI fails at the install step on those jobs only. The fix is to split the requirement with PEP 508 `python_version` markers so each Python gets the newest release it can install. Dropping an old Python to turn CI green is not the fix.
-Every `requirements/*.txt` file in this repo follows one layout convention and one version-constraint pattern. This keeps a single set of pins working across the whole Python matrix and every diff readable. The mechanism for making one line behave differently per interpreter is [PEP 508](https://peps.python.org/pep-0508/) environment markers. That spec is the authority for the marker syntax used throughout this skill.
+## Where the facts live
-Two facts about this repo drive everything below:
+Read these instead of trusting memory; they change often.
-- It supports **Python `>=3.7`** (`requires-python` and the classifiers in `pyproject.toml`). Its CI matrix in `.github/workflows/ci-build.yml` tests **CPython 3.7 through 3.14 only** — there is **no PyPy** (the classifiers list only `Programming Language :: Python :: Implementation :: CPython`).
-- Many popular packages keep raising their minimum Python. A bump that raises a dependency's **lower bound** to a release requiring a newer Python makes pip's resolver find nothing installable on the old interpreters. The install step then fails there before tests even run.
+- **Supported Pythons:** `requires-python` and the `Programming Language :: Python :: 3.X` classifiers in `pyproject.toml`, and the CI matrix.
+- **Who installs a file, on which Python:** `grep -rn requirements .github/workflows scripts requirements pyproject.toml`. This also catches `-r` includes. For a hit in a script, find the CI job that runs it and read its `python-version`.
+- **Whether a file ships to users:** a file that `pyproject.toml` reads into package metadata (`dependencies` or `optional-dependencies` under `[tool.setuptools.dynamic]`) ends up in the published wheel.
-The convention resolves this without dropping old-Python support: pin each interpreter to the newest release it can actually install, using PEP 508 `python_version` markers.
+## Workflow
-## File-layout convention
+- [ ] 1. Find which Pythons install the file. A file installed only on the newest Python takes the bump as-is.
+- [ ] 2. Find the newest release each affected Python can install (see "Finding the ceiling").
+- [ ] 3. If every Python the line covers can install the new lower bound, take the bump as one line. Done.
+- [ ] 4. Otherwise split the line (see "Splitting a line").
+- [ ] 5. Re-read the section: no Python matches two lines, and the newest line is open-ended.
-Each file starts with a `# pip install -r requirements/.txt` header, then lists **one dependency per section**: the name of the dependency (as a `# name` header), an optional rationale note (starting with `# Note:`, explaining why a version is pinned or split), then the requirement line(s), separated from the next section by a blank line. This makes every pin self-documenting.
+Change only the section you are fixing; don't reformat other sections in the same change.
-```
-# pip install -r requirements/test.txt
+## File layout
-# pytest
-pytest<9.2
+Each file starts with a `# pip install -r ` header, then one section per dependency: a `# ` line, optional `# Note:` lines, the requirement line(s), and a blank line.
-# pytest-cov
-# Note: only needed to evaluate coverage on the latest supported python version
-pytest-cov>=7.1.0,<8; python_version >= "3.14"
```
+# pip install -r requirements/testing.txt
-Keep this layout when adding or editing dependencies. Never leave an empty trailing `;` (a fossil of a collapsed split; delete it — the old `pytest-asyncio<2;` line was exactly this).
-
-## Which files need Python-version markers
-
-A marker split is only needed for requirements files installed across the **full** Python matrix. Which file you are editing decides this. To see where a file is installed, read `.github/workflows/ci-build.yml`. It is the source of truth for which Python versions install which requirements files. Everything except `dev_tools.txt` is installed by the `unittest` matrix job across 3.7–3.14.
-
-| File | Installed on | Needs markers? |
-| ----------------------------- | ----------------------------------------------------- | ----------------------------------- |
-| `requirements/adapter_dev.txt` | full matrix (`unittest`; also `typecheck`/`codecov` @3.14) | **Yes, if a bump raises the floor** |
-| `requirements/async_dev.txt` | full matrix (`unittest`; also `typecheck`/`codecov` @3.14) | **Yes, if a bump raises the floor** |
-| `requirements/test.txt` | full matrix (`unittest`) | **Yes, if a bump raises the floor** |
-| `requirements/test_adapter.txt` | full matrix (`unittest`; `codecov` @3.14) | **Yes, if a bump raises the floor** |
-| `requirements/test_async.txt` | full matrix (`unittest`) | **Yes, if a bump raises the floor** |
-| `requirements/dev_tools.txt` | `lint` + `typecheck`, **latest Python only** (3.14) | No, just take the bump (`==` pins) |
-
-If the bump lands in a latest-Python-only file, take it as-is: no markers, no ceiling, just the layout convention above.
-
-Unlike some sibling repos, bolt-python has **no requirements file that feeds packaged wheel metadata**: `pyproject.toml` has no `[project.optional-dependencies]`, and `[tool.setuptools.dynamic]` resolves only `version` and `readme`. So there is no "special" file whose comments leak into a wheel — every `requirements/*.txt` file is dev/test-only.
+# pkg
+pkg>=1.4,<2; python_version < "3.9"
+pkg>=2.0,<2.3; python_version == "3.9"
+pkg>=2.5.1,<3; python_version >= "3.10"
-## The version-constraint pattern
-
-When a dependency's floor rises to a release that requires a newer Python, **do not** just take the bump, and **do not** drop old-Python support to make CI pass. Instead, split the requirement into `python_version`-marked lines that **partition the whole matrix**. Every interpreter matches exactly one line. Old interpreters keep the last compatible release (with an explicit ceiling), and the newest line is open-ended so future Pythons stay covered.
-
-```
-# aiohttp
-aiohttp>=3,<4; python_version < "3.9"
-aiohttp>=3.13.5,<4; python_version >= "3.9"
+# other-pkg
+# Note: other-pkg 4.x breaks on Python 3.8 but declares no requires_python, so pip can't filter it.
+other-pkg>=3,<4
```
-`aiohttp` 3.13.5 requires Python `>=3.9`, so Python 3.7/3.8 stay on the older line. The same 3.9 split appears for `falcon`, `fastapi`, `Flask`, `Werkzeug`, `starlette`, `tornado`, `websocket_client`, and (in `test_async.txt`) `asgiref`. The `Django` split lands at 3.8 instead — Django 4.x requires `>=3.8`, so 3.7 keeps the 3.2 line:
-
-```
-# Django
-Django>=3.2,<4; python_version < "3.8"
-Django>=4.2.30,<6; python_version >= "3.8"
-```
+**Notes:** write one only for what the lines can't say: a cap that isn't obvious, a coupling with another package, or a release with wrong metadata. Don't restate versions or which Python gets which release; a routine split needs no note. Existing notes that only restate their lines are legacy: drop one when you edit its section, and don't copy it.
-## Canonical marker style
+## Finding the ceiling
-Consistency matters because these lines are read and edited often, and a stray style makes diffs noisy. Standardize on this:
+An old Python's ceiling is the first release that dropped it, not the release Dependabot proposes. If `pkg` dropped 3.9 at 2.3 and the target is 2.5.1, the 3.9 line is `<2.3`, since 2.3 through 2.5.0 are 3.10-only too.
-- Spaces around every operator in the marker: `python_version >= "3.9"`, never `python_version>="3.9"`.
-- Double-quoted `major.minor` string: `"3.9"`. (`packaging` compares these version-aware, so `python_version >= "3.9"` correctly includes 3.10–3.14, no lexicographic surprise.)
-- Use `>=` / `<` for the Python boundary; avoid `>` / `<=` so the boundary version lands on exactly one side. **This rule is about the `python_version` marker, not the version specifier** — `boto3<=2` and `cheroot<12` are correct as written.
-- One space after the `;`, none before: `pkg>=1,<2; python_version >= "3.9"`.
-- The old-side line always carries an explicit upper bound (the floor-jump version).
-- The marker set must be **exhaustive and mutually exclusive** across the matrix. The newest line ends open-ended (`>= "X.Y"`), never a bare `==` that leaves future Pythons unmatched.
+- **From a red CI job:** the failing install log is ground truth. The lowest version on its `Ignored the following versions that require a different python version` line is that Python's ceiling.
+- **For any Python:** `pip index versions --python-version 3.9` lists what 3.9 can install, newest first. Run it again for the next Python up; the first release newer than 3.9's newest is 3.9's ceiling.
+- **The pre-bump lower bound** is in the Dependabot PR title (`update pkg requirement from <3,>=2.0 to >=2.5.1,<3`) or `git diff`.
-## Deriving the versions to pin
+## Splitting a line
-You need two numbers: the **floor** (which Python the new release requires) and the old-side **ceiling** (the first release that raised that floor).
+Split only the Pythons that the bumped line covers and the new lower bound excludes. Leave existing bands for older Pythons alone, even when they are coarser than they could be.
-1. **Floor.** Read the metadata for the _exact target version_ at `https://pypi.org/pypi///json`. The `info.requires_python` field gives the new minimum (e.g. `">=3.9"`). A `null` there means the release declares no floor.
+- Lines run oldest Python to newest. The newest line takes the bump and stays open-ended (`>= "X.Y"`).
+- Each band that splits off keeps the line's pre-bump lower bound and gets its ceiling from "Finding the ceiling". Pythons that share a ceiling share a band.
+- Marker style: `pkg>=1,<2; python_version == "3.9"`. Spaces around operators, double-quoted `"X.Y"`, one space after `;`. A one-Python band is `== "X.Y"`; a band spanning several is `>= "X.Y" and < "X.Z"`, or `< "X.Z"` when it is the oldest. Boundaries use `>=` and `<` so each Python lands on exactly one side. `python_version` is major.minor only (3.9.5 reports `"3.9"`), so `== "3.9"` covers every 3.9 patch; don't use `python_full_version` here, where `== "3.9"` matches only 3.9.0.
-2. **Ceiling.** Walk the release history at `https://pypi.org/pypi//json` and find the **first version that raised the floor above the oldest matrix Python**. The old-side ceiling is `< `. For example, if a package jumped to `>=3.9` at version **4.0.0**, the old-side cap is `<4` even if the target is `4.2.0` (pinning `<4.2.0` would wrongly admit 4.0.0–4.1.x, which are also 3.9-only).
+A section with a single gated line (installed only on the newest Python, or only on PyPy) is intentional; don't fill in the other Pythons.
-Why an explicit ceiling instead of trusting pip to filter by `Requires-Python`? Because that filtering only holds if every future release keeps its metadata correct; a single mis-tagged release would silently float onto an untested interpreter. An explicit ceiling makes the intent self-documenting and robust.
-
-`tracerite` is the cautionary case: its releases after 1.1.2 break on Python `<= 3.8`, yet those releases publish **no `requires_python` metadata at all** (verify: `https://pypi.org/pypi/tracerite/1.1.3/json` shows `requires_python: null`). So pip filtering offers zero protection on the old interpreters, and the explicit `tracerite<1.1.2` ceiling is load-bearing, not decorative.
-
-**If you arrived here from a red CI job:** the failing _install_ log is ground truth. It names the interpreter that failed and the versions pip was actually offered, e.g.:
-
-```
-ERROR: Ignored the following versions that require a different python version: 4.2.0 Requires-Python >=3.9
-ERROR: Could not find a version that satisfies the requirement falcon>=4.2.0 (from versions: ..., 3.1.3)
-```
-
-Cross-check the PyPI value against that log so you are never guessing. (If instead the failure is a real test failure, or hits _every_ Python version, this pattern does not apply, so investigate the bump normally.)
-
-## One harder shape: a coupled companion dependency
-
-Sometimes a package drags in another distribution with loose or wildcard versions that you must co-pin on the same boundary. `Sanic` imports `tracerite` with wildcard versions, so `tracerite` is pinned right beside it:
-
-```
-# sanic
-# Note: Sanic pulls in tracerite via a wildcard version, so tracerite is co-pinned here.
-# Note: tracerite > 1.1.2 is incompatible with Python <= 3.8 and ships no requires_python, so an explicit ceiling is required.
-tracerite<1.1.2; python_version < "3.9"
-sanic>=21,<24; python_version < "3.9"
-sanic>=25.3.0,<26; python_version >= "3.9"
-```
+## Gotchas
-The split boundary here (3.9) is driven by `tracerite`, **not** by Sanic itself — Sanic 25.3 supports Python 3.8 (`requires_python: >=3.8`). Python 3.8 is kept on old Sanic only because tracerite breaks there. When a companion transitive dependency is the thing that breaks, pin it explicitly rather than hoping the parent's resolver picks a compatible version, and keep it in the same section so the coupling stays visible.
+- **Dependabot rewrites only the last line** of a split section, the open-ended one. When it bumps a split package, check which Pythons that line covers; the new lower bound may exclude the oldest of them, which then splits off.
+- **Releases with no `requires_python`** can't be filtered by pip, so they install on Pythons they don't support and fail at import or test time rather than at install. `pip index` lists them too. Pin an explicit ceiling with a note saying why. When the culprit comes in loosely through another package, pin it in that package's section so the coupling stays visible.
+- **Only PyPy jobs fail:** the release usually dropped PyPy wheels. Gate on the implementation instead of downgrading CPython: `pkg<46; implementation_name == "pypy" and python_version == "3.10"`. Lowercase `pypy`, and `implementation_name`, not `platform_python_implementation`. No unmarked companion line is needed.
+- **Files that ship:** full-line comments only; an inline comment breaks the package build. Their lower bounds are the minimum users must install, so raise one only for a reason.
+- **Not this pattern:** a real test failure, or an install failure on every Python. Investigate the bump normally.
-## Collapse when a Python is dropped
+## Dropping a Python version
-Marker splits are maintenance cost, so remove them when they stop earning their keep. When a Python version is dropped from the CI matrix (and from `requires-python` / the classifiers), collapse any split whose only reason was that version back into a single unmarked line, and delete the trailing `;`. For example, if 3.7 is dropped, the `Django>=3.2,<4; python_version < "3.8"` line has no interpreter left to serve, and the section collapses to a single `Django` line. A leaner file is easier for both humans and Dependabot to reason about.
+When a Python leaves the classifiers and the CI matrix, merge any band that only served it into its neighbor. If a section is left with one line, remove its marker and the `;`.
-## What to leave alone
+## Leave alone
-- **Do not touch `requires-python` or the CI matrix.** Keeping 3.7 working on old dependency versions is the entire point; changing the floor is a separate, deliberate decision.
-- **Do not add runtime dependencies to `pyproject.toml`.** The core package depends only on `slack_sdk` (see the "Single Runtime Dependency Rule" in `AGENTS.md`); everything else belongs in `requirements/*.txt`.
-- **Prefer markers over a Dependabot `ignore`.** An `ignore` rule freezes newer Pythons on the old version too, and hides the version knowledge in config. Reserve `ignore` for the rare dep that must stay pinned everywhere for reproducible output.
+- `requires-python`, the classifiers, and the CI matrix. Changing which Pythons are supported is its own decision.
+- Packages under `ignore` in `.github/dependabot.yml`: maintainers bump those by hand on purpose. Prefer a marker split over adding a new `ignore`, which freezes the package on every Python.
diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
index 4a08579c2..f152f56e8 100644
--- a/.github/CODEOWNERS
+++ b/.github/CODEOWNERS
@@ -1,7 +1,7 @@
# Salesforce Open Source project configuration
# Learn more: https://github.com/salesforce/oss-template
#ECCN:Open Source
-#GUSINFO:Open Source,Open Source Workflow
+#GUSINFO:Slack Developer Experience,Open Source
# @slackapi/slack-platform-python
# are code reviewers for all changes in this repo.
diff --git a/.github/workflows/ci-build.yml b/.github/workflows/ci-build.yml
index dd2119b8c..0cb3e60d1 100644
--- a/.github/workflows/ci-build.yml
+++ b/.github/workflows/ci-build.yml
@@ -10,7 +10,7 @@ on:
workflow_dispatch:
env:
- LATEST_SUPPORTED_PY: "3.14"
+ LATEST_SUPPORTED_PY: "3.15"
jobs:
lint:
@@ -24,7 +24,7 @@ jobs:
with:
persist-credentials: false
- name: Set up Python ${{ env.LATEST_SUPPORTED_PY }}
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
+ uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0
with:
python-version: ${{ env.LATEST_SUPPORTED_PY }}
- name: Run lint verification
@@ -41,7 +41,7 @@ jobs:
with:
persist-credentials: false
- name: Set up Python ${{ env.LATEST_SUPPORTED_PY }}
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
+ uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0
with:
python-version: ${{ env.LATEST_SUPPORTED_PY }}
- name: Install synchronous dependencies
@@ -74,6 +74,7 @@ jobs:
- "3.12"
- "3.13"
- "3.14"
+ - "3.15"
permissions:
contents: read
steps:
@@ -81,7 +82,7 @@ jobs:
with:
persist-credentials: false
- name: Set up Python ${{ matrix.python-version }}
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
+ uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0
with:
python-version: ${{ matrix.python-version }}
- name: Install synchronous dependencies
@@ -126,7 +127,7 @@ jobs:
pytest tests/scenario_tests_async/ --junitxml=reports/test_scenario_async.xml
- name: Upload test results to Codecov
if: ${{ !cancelled() }}
- uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0
+ uses: codecov/codecov-action@303a32d7a59b442fa8d48b6a1cc6825c09c847a5 # v7.1.1
with:
directory: ./reports/
fail_ci_if_error: false
@@ -148,7 +149,7 @@ jobs:
with:
persist-credentials: false
- name: Set up Python ${{ env.LATEST_SUPPORTED_PY }}
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
+ uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0
with:
python-version: ${{ env.LATEST_SUPPORTED_PY }}
- name: Install dependencies
@@ -162,7 +163,7 @@ jobs:
run: |
pytest --cov=./slack_bolt/ --cov-report=xml
- name: Upload coverage to Codecov
- uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0
+ uses: codecov/codecov-action@303a32d7a59b442fa8d48b6a1cc6825c09c847a5 # v7.1.1
with:
fail_ci_if_error: true
report_type: coverage
diff --git a/.github/workflows/pypi-release.yml b/.github/workflows/pypi-release.yml
index 2d0f7630a..7ef27db4d 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@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
+ uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0
with:
python-version: "3.x"
@@ -33,7 +33,7 @@ jobs:
scripts/build_pypi_package.sh
- name: Persist dist folder
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
+ uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: release-dist
path: dist/
@@ -52,7 +52,7 @@ jobs:
steps:
- name: Retrieve dist folder
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
+ uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
with:
name: release-dist
path: dist/
@@ -60,7 +60,7 @@ jobs:
- name: Publish release distributions to test.pypi.org
# Using OIDC for PyPI publishing (no API tokens needed)
# See: https://docs.github.com/en/actions/how-tos/secure-your-work/security-harden-deployments/oidc-in-pypi
- uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2
+ uses: pypa/gh-action-pypi-publish@7245906fb61fa33a13554f9d5fb856fe61758086 # v1.14.3
with:
repository-url: https://test.pypi.org/legacy/
@@ -76,7 +76,7 @@ jobs:
steps:
- name: Retrieve dist folder
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
+ uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
with:
name: release-dist
path: dist/
@@ -84,4 +84,4 @@ jobs:
- name: Publish release distributions to pypi.org
# Using OIDC for PyPI publishing (no API tokens needed)
# See: https://docs.github.com/en/actions/how-tos/secure-your-work/security-harden-deployments/oidc-in-pypi
- uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2
+ uses: pypa/gh-action-pypi-publish@7245906fb61fa33a13554f9d5fb856fe61758086 # v1.14.3
diff --git a/AGENTS.md b/AGENTS.md
index cc6ba8198..37084fa15 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -214,7 +214,7 @@ The core package has a **single required runtime dependency**: `slack_sdk` (defi
When adding a new dependency: add it to the appropriate `requirements/*.txt` file with version constraints, never to `pyproject.toml` `dependencies` (unless it's a core runtime dep, which is very rare).
-Before adding, bumping, pinning, or reviewing any dependency in `requirements/*.txt` -- whether a Dependabot PR or a manual edit -- follow the `managing-dependencies` skill in `.claude/skills/`. It defines the layout and `python_version` marker conventions that keep one set of pins working across the full CPython 3.7--3.14 matrix.
+Before adding, bumping, pinning, or reviewing any dependency in `requirements/*.txt` -- whether a Dependabot PR or a manual edit -- follow the `managing-dependencies` skill in `.claude/skills/`. It defines the layout and `python_version` marker conventions that keep one set of pins working across the full CPython 3.7--3.15 matrix.
## Test Organization and CI
@@ -236,7 +236,7 @@ GitHub Actions (`.github/workflows/ci-build.yml`) runs on every push to `main` a
- **Lint** -- `./scripts/lint.sh` on latest Python
- **Typecheck** -- `./scripts/run_mypy.sh` on latest Python
-- **Unit tests** -- full test suite across Python 3.7--3.14 matrix
+- **Unit tests** -- full test suite across Python 3.7--3.15 matrix
- **Code coverage** -- uploaded to Codecov
## PR and Commit Guidelines
diff --git a/README.md b/README.md
index 39747df40..d46490693 100644
--- a/README.md
+++ b/README.md
@@ -14,7 +14,7 @@
-A Python framework to build Slack apps in a flash with the latest platform features. Read the [getting started guide](https://docs.slack.dev/tools/bolt-python/getting-started) and look at our [code examples](https://github.com/slackapi/bolt-python/tree/main/examples) to learn how to build apps using Bolt. The Python module documents are available [here](https://docs.slack.dev/tools/bolt-python/reference/).
+A Python framework to build Slack apps in a flash with the latest platform features. Read the [Creating an app guide](https://docs.slack.dev/tools/bolt-python/creating-an-app) and look at our [code examples](https://github.com/slackapi/bolt-python/tree/main/examples) to learn how to build apps using Bolt. The Python module documents are available [here](https://docs.slack.dev/tools/bolt-python/reference/).
## Setup
diff --git a/docs/english/_sidebar.json b/docs/english/_sidebar.json
index aa75b0c15..22a8ba69d 100644
--- a/docs/english/_sidebar.json
+++ b/docs/english/_sidebar.json
@@ -5,8 +5,6 @@
"label": "Bolt for Python",
"className": "sidebar-title"
},
- "tools/bolt-python/getting-started",
- { "type": "html", "value": "
" },
"tools/bolt-python/creating-an-app",
{
"type": "category",
@@ -102,7 +100,6 @@
"label": "Legacy",
"items": ["tools/bolt-python/legacy/steps-from-apps"]
},
- { "type": "html", "value": "
" },
{
"type": "category",
"label": "Tutorials",
@@ -116,13 +113,11 @@
"tools/bolt-python/tutorial/modals/modals"
]
},
- { "type": "html", "value": "
" },
{
"type": "link",
"label": "Reference",
"href": "https://docs.slack.dev/tools/bolt-python/reference/index.html"
},
- { "type": "html", "value": "
" },
{
"type": "category",
"label": "日本語 (日本)",
diff --git a/docs/english/concepts/custom-steps-dynamic-options.md b/docs/english/concepts/custom-steps-dynamic-options.md
index 9a152daa0..dfe95a323 100644
--- a/docs/english/concepts/custom-steps-dynamic-options.md
+++ b/docs/english/concepts/custom-steps-dynamic-options.md
@@ -88,13 +88,13 @@ The `inputs` attribute defines the parameters to be passed as inputs to the step
The following format can be used to reference any input parameter defined by the step: `{{input_parameters.}}`.
-In addition, the `{{client.query}}` parameter can be used as a placeholder for an input value. The `{{client.builder_context}}` parameter will inject the [`slack#/types/user_context`](/tools/deno-slack-sdk/reference/slack-types/#usercontext) of the user building the workflow as the value to the input parameter.
+In addition, the `{{client.query}}` parameter can be used as a placeholder for an input value. The `{{client.builder_context}}` parameter will inject the [`slack#/types/user_context`](/reference/types/user_context-type) of the user building the workflow as the value to the input parameter.
### Types of dynamic options UIs {#dynamic-option-UIs}
The above example demonstrates one possible UI to be rendered for builders: a single-select drop-down menu of dynamic options. However, dynamic options in Workflow Builder can be rendered in one of two ways: as a drop-down menu (single-select or multi-select), or as a set of fields.
-The type is dictated by the output parameter of the custom step used as a dynamic option. In order to use a custom step in a dynamic option context, its output must adhere to a defined interface, that is, it must have an `options` parameter of type [`options_select`](/tools/deno-slack-sdk/reference/slack-types#options_select) or [`options_field`](/tools/deno-slack-sdk/reference/slack-types#options_field), as shown in the following code snippet.
+The type is dictated by the output parameter of the custom step used as a dynamic option. In order to use a custom step in a dynamic option context, its output must adhere to a defined interface, that is, it must have an `options` parameter of type [`options_select`](/reference/types/options_select-type) or [`options_field`](/reference/types/options_field-type), as shown in the following code snippet.
```js
"output_parameters": {
@@ -109,9 +109,9 @@ The type is dictated by the output parameter of the custom step used as a dynami
#### Drop-down menus {#drop-down}
-Your dynamic input parameter can be rendered as a drop-down menu, which will use the options obtained from a custom step with an `options` output parameter of the type [`options_select`](/tools/deno-slack-sdk/reference/slack-types#options_select).
+Your dynamic input parameter can be rendered as a drop-down menu, which will use the options obtained from a custom step with an `options` output parameter of the type [`options_select`](/reference/types/options_select-type).
-The drop-down menu UI component can be rendered in two ways: single-select, or multi-select. To render the dynamic input as a single-select menu, the input parameter defining the dynamic option must be of the type [`string`](/tools/deno-slack-sdk/reference/slack-types#string).
+The drop-down menu UI component can be rendered in two ways: single-select, or multi-select. To render the dynamic input as a single-select menu, the input parameter defining the dynamic option must be of the type [`string`](/reference/types/string-type).
```js
"step-with-dynamic-input": {
@@ -133,7 +133,7 @@ The drop-down menu UI component can be rendered in two ways: single-select, or m
}
```
-To render the dynamic input as a multi-select menu, the input parameter defining the dynamic option must be of the type [`array`](/tools/deno-slack-sdk/reference/slack-types#array), and its `items` must be of type [`string`](/tools/deno-slack-sdk/reference/slack-types#string).
+To render the dynamic input as a multi-select menu, the input parameter defining the dynamic option must be of the type [`array`](/reference/types/array-type), and its `items` must be of type [`string`](/reference/types/string-type).
```js
"step-with-dynamic-input": {
@@ -159,9 +159,9 @@ To render the dynamic input as a multi-select menu, the input parameter defining
#### Fields {#fields}
-In the code snippet below, the input parameter is rendered as a set of fields with keys and values. The option fields are obtained from a custom step with an `options` output parameter of type [`options_field`](/tools/deno-slack-sdk/reference/slack-types#options_field).
+In the code snippet below, the input parameter is rendered as a set of fields with keys and values. The option fields are obtained from a custom step with an `options` output parameter of type [`options_field`](/reference/types/options_field-type).
-The input parameter that defines the dynamic option must be of type [`object`](/tools/deno-slack-sdk/reference/slack-types#object), as the completed set of fields in Workflow Builder will be passed to the custom step as an [untyped object](/tools/deno-slack-sdk/reference/slack-types#untyped-object) during workflow execution.
+The input parameter that defines the dynamic option must be of type [`object`](/reference/types/object-type), as the completed set of fields in Workflow Builder will be passed to the custom step as an [untyped object](/reference/types/object-type#untyped-object) during workflow execution.
```js
"test-field-dynamic-options": {
@@ -185,15 +185,15 @@ The input parameter that defines the dynamic option must be of type [`object`](/
### Dynamic option types {#dynamic-option-types}
-As mentioned earlier, in order to use a custom step as a dynamic option, its output must adhere to a defined interface: it must have an `options` output parameter of the type either [`options_select`](/tools/deno-slack-sdk/reference/slack-types#options_select) or [`options_field`](/tools/deno-slack-sdk/reference/slack-types#options_field).
+As mentioned earlier, in order to use a custom step as a dynamic option, its output must adhere to a defined interface: it must have an `options` output parameter of the type either [`options_select`](/reference/types/options_select-type) or [`options_field`](/reference/types/options_field-type).
-To take a look at these in more detail, refer to our [Options Slack type](/tools/deno-slack-sdk/reference/slack-types#options) documentation.
+To take a look at these in more detail, refer to our [Options Slack type](/reference/types) documentation.
## Dynamic options handler {#dynamic-option-handler}
Each custom step defined in the manifest needs a corresponding handler in your Slack app. Although implemented similarly to existing function execution event handlers, there are two key differences between regular custom step invocations and those used for dynamic options:
-* The custom step must have an `options` output parameter that is of type [`options_select`](/tools/deno-slack-sdk/reference/slack-types#options_select) or [`options_field`](/tools/deno-slack-sdk/reference/slack-types#options_field).
+* The custom step must have an `options` output parameter that is of type [`options_select`](/reference/types/options_select-type) or [`options_field`](/reference/types/options_field-type).
* The [`function_executed`](/reference/events/function_executed) event must be handled synchronously. This optimizes the response time of returned dynamic options and provides a crisp builder experience.
### Asynchronous event handling {#async}
diff --git a/docs/english/concepts/message-sending.md b/docs/english/concepts/message-sending.md
index c4d1b0467..67b70d9b0 100644
--- a/docs/english/concepts/message-sending.md
+++ b/docs/english/concepts/message-sending.md
@@ -17,7 +17,7 @@ def ask_who(message, say):
`say()` accepts more complex message payloads to make it easy to add functionality and structure to your messages.
-To explore adding rich message layouts to your app, read through [the guide on our API site](/messaging/#structure) and look through templates of common app flows [in the Block Kit Builder](https://api.slack.com/tools/block-kit-builder?template=1).
+To explore adding rich message layouts to your app, read through [the guide on our API site](/messaging/#structure) and look through templates of common app flows [in the Block Kit Builder](https://app.slack.com/block-kit-builder).
```python
# Sends a section block with datepicker when someone reacts with a 📅 emoji
diff --git a/docs/english/concepts/shortcuts.md b/docs/english/concepts/shortcuts.md
index b28f0b352..86186e035 100644
--- a/docs/english/concepts/shortcuts.md
+++ b/docs/english/concepts/shortcuts.md
@@ -40,7 +40,7 @@ def open_modal(ack, shortcut, client):
"elements": [
{
"type": "mrkdwn",
- "text": "Psssst this modal was designed using "
+ "text": "Psssst this modal was designed using "
}
]
}
@@ -79,7 +79,7 @@ def open_modal(ack, shortcut, client):
"elements": [
{
"type": "mrkdwn",
- "text": "Psssst this modal was designed using "
+ "text": "Psssst this modal was designed using "
}
]
}
diff --git a/docs/english/creating-an-app.md b/docs/english/creating-an-app.md
index 66e1febee..0901588a8 100644
--- a/docs/english/creating-an-app.md
+++ b/docs/english/creating-an-app.md
@@ -4,10 +4,14 @@ sidebar_label: Creating an app
# Creating an app with Bolt for Python
-This guide is meant to walk you through getting up and running with a Slack app using Bolt for Python. Along the way, we’ll create a new Slack app, set up your local environment, and develop an app that listens and responds to messages from a Slack workspace.
+:::tip[More doing, less reading]
+This guide is a more thorough explanation on creating a Slack app. If you want a running app as fast as possible, follow our [Quickstart](/quickstart) that harnesses the power of Bolt and the [Slack CLI](/tools/slack-cli).
+:::
+
+Follow along to get up and running with a Slack app using Bolt for Python. Along the way, we’ll create a new Slack app, set up your local environment, and develop an app that listens and responds to messages from a Slack workspace.
When you're finished, you'll have created the [Getting Started app](https://github.com/slackapi/bolt-python/tree/main/examples/getting_started) to run, modify, and make your own. ⚡️
-
+
---
### Create a new app {#create-an-app}
@@ -15,7 +19,7 @@ First thing's first: before you start developing with Bolt, you'll want to [crea
:::tip[A place to test and learn]
-We recommend using a workspace where you won't disrupt real work getting done — [you can create a new one for free](https://slack.com/get-started#create).
+We recommend using a workspace where you won't disrupt real work getting done, like a [developer sandbox](/tools/developer-sandboxes).
:::
diff --git a/docs/english/getting-started.md b/docs/english/getting-started.md
deleted file mode 100644
index 8cfd7faf8..000000000
--- a/docs/english/getting-started.md
+++ /dev/null
@@ -1,288 +0,0 @@
----
-sidebar_label: Quickstart
-title: Quickstart guide with Bolt for Python
----
-
-This quickstart guide aims to help you get a Slack app using Bolt for Python up and running as soon as possible!
-
-import Tabs from '@theme/Tabs';
-import TabItem from '@theme/TabItem';
-
-When complete, you'll have a local environment configured with a customized [app](https://github.com/slack-samples/bolt-python-getting-started-app) running to modify and make your own.
-
-:::tip[Reference for readers]
-
-In search of the complete guide to building an app from scratch? Check out the [building an app](/tools/bolt-python/creating-an-app) guide.
-
-:::
-
-#### Prerequisites
-
-A few tools are needed for the following steps. We recommend using the [**Slack CLI**](/tools/slack-cli/) for the smoothest experience, but other options remain available.
-
-You can also begin by installing git and downloading [Python 3.7 or later](https://www.python.org/downloads/), or the latest stable version of Python. Refer to [Python's setup and building guide](https://devguide.python.org/getting-started/setup-building/) for more details.
-
-Install the latest version of the Slack CLI to get started:
-
-- [Slack CLI for macOS & Linux](/tools/slack-cli/guides/installing-the-slack-cli-for-mac-and-linux)
-- [Slack CLI for Windows](/tools/slack-cli/guides/installing-the-slack-cli-for-windows)
-
-Then confirm a successful installation with the following command:
-
-```sh
-$ slack version
-```
-
-An authenticated login is also required if this hasn't been done before:
-
-```sh
-$ slack login
-```
-
-:::info[A place to belong]
-
-A workspace where development can happen is also needed.
-
-We recommend using [developer sandboxes](/tools/developer-sandboxes) to avoid disruptions where real work gets done.
-
-:::
-
-## Creating a project {#creating-a-project}
-
-With the toolchain configured, it's time to set up a new Bolt project. This contains the code that handles logic for your app.
-
-If you don’t already have a project, let’s create a new one!
-
-
-
-
-A starter template can be used to start with project scaffolding:
-
-```sh
-$ slack create first-bolt-app --template slack-samples/bolt-python-getting-started-app
-$ cd first-bolt-app
-```
-
-After a project is created you'll have a `requirements.txt` file for app dependencies and a `.slack` directory for Slack CLI configuration.
-
-A few other files exist too, but we'll visit these later.
-
-
-
-
-A starter template can be cloned to start with project scaffolding:
-
-```sh
-$ git clone https://github.com/slack-samples/bolt-python-getting-started-app first-bolt-app
-$ cd first-bolt-app
-```
-
-Outlines of a project are taking shape, so we can move on to running the app!
-
-
-
-
-We recommend using a [Python virtual environment](https://packaging.python.org/guides/installing-using-pip-and-virtual-environments/#creating-a-virtual-environment) to manage your project's dependencies. This is a great way to prevent conflicts with your system's Python packages. Let's create and activate a new virtual environment with [Python 3.7 or later](https://www.python.org/downloads/):
-
-```sh
-$ python3 -m venv .venv
-$ source .venv/bin/activate
-$ pip install -r requirements.txt
-```
-
-Confirm the virtual environment is active by checking that the path to `python3` is _inside_ your project ([a similar command is available on Windows](https://packaging.python.org/guides/installing-using-pip-and-virtual-environments/#activating-a-virtual-environment)):
-
-```sh
-$ which python3
-# Output: /path/to/first-bolt-app/.venv/bin/python3
-```
-
-## Running the app {#running-the-app}
-
-Before you can start developing with Bolt, you will want a running Slack app.
-
-
-
-
-The getting started app template contains a `manifest.json` file with details about an app that we will use to get started. Use the following command and select "Create a new app" to install the app to the team of choice:
-
-```sh
-$ slack run
-...
-⚡️ Bolt app is running!
-```
-
-With the app running, you can test it out with the following steps in Slack:
-
-1. Open a direct message with your app or invite the bot `@first-bolt-app (local)` to a public channel.
-2. Send "hello" to the current conversation and wait for a response.
-3. Click the attached button labelled "Click Me" to post another reply.
-
-After confirming the app responds, celebrate, then interrupt the process by pressing `CTRL+C` in the terminal to stop your app from running.
-
-
-
-
-Navigate to your list of apps and [create a new Slack app](https://api.slack.com/apps/new) using the "from a manifest" option:
-
-1. Select the workspace to develop your app in.
-2. Copy and paste the `manifest.json` file contents to create your app.
-3. Confirm the app features and click "Create".
-
-You'll then land on your app's **Basic Information** page, which is an overview of your app and which contains important credentials:
-
-
-
-To listen for events happening in Slack (such as a new posted message) without opening a port or exposing an endpoint, we will use [Socket Mode](/tools/bolt-python/concepts/socket-mode). This connection requires a specific app token:
-
-1. On the **Basic Information** page, scroll to the **App-Level Tokens** section and click **Generate Token and Scopes**.
-2. Name the token "Development" or something similar and add the `connections:write` scope, then click **Generate**.
-3. Save the generated `xapp` token as an environment variable within your project:
-
-```sh
-$ export SLACK_APP_TOKEN=
-```
-
-The above command works on Linux and macOS but [similar commands are available on Windows](https://superuser.com/questions/212150/how-to-set-env-variable-in-windows-cmd-line/212153#212153).
-
-:::warning[Keep it secret. Keep it safe.]
-
-Treat your tokens like a password and [keep it safe](/concepts/security). Your app uses these to retrieve and send information to Slack.
-
-:::
-
-A bot token is also needed to interact with the Web API methods as your app's bot user. We can gather this as follows:
-
-1. Navigate to the **OAuth & Permissions** on the left sidebar and install your app to your workspace to generate a token.
-2. After authorizing the installation, you'll return to the **OAuth & Permissions** page and find a **Bot User OAuth Token**:
-
-
-
-3. Copy the bot token beginning with `xoxb` from the **OAuth & Permissions page** and then store it in a new environment variable:
-
-```sh
-$ export SLACK_BOT_TOKEN=xoxb-
-```
-
-After saving tokens for the app you created, it is time to run it:
-
-```sh
-$ python3 app.py
-...
-⚡️ Bolt app is running!
-```
-
-With the app running, you can test it out with the following steps in Slack:
-
-1. Open a direct message with your app or invite the bot `@BoltApp` to a public channel.
-2. Send "hello" to the current conversation and wait for a response.
-3. Click the attached button labelled "Click Me" to post another reply.
-
-After confirming the app responds, celebrate, then interrupt the process by pressing `CTRL+C` in the terminal to stop your app from running.
-
-
-
-
-## Updating the app
-
-At this point, you've successfully run the getting started Bolt for Python [app](https://github.com/slack-samples/bolt-python-getting-started-app)!
-
-The defaults included leave opportunities abound, so to personalize this app let's now edit the code to respond with a kind farewell.
-
-#### Responding to a farewell
-
-Chat is a common thing apps do and responding to various types of messages can make conversations more interesting.
-
-Using an editor of choice, open the `app.py` file and add the following import to the top of the file, and message listener after the "hello" handler:
-
-```python
-import random
-
-@app.message("goodbye")
-def message_goodbye(say):
- responses = ["Adios", "Au revoir", "Farewell"]
- parting = random.choice(responses)
- say(f"{parting}!")
-```
-
-Once the file is updated, save the changes and then we'll make sure those changes are being used.
-
-
-
-
-Run the following command and select the app created earlier to start, or restart, your app with the latest changes:
-
-```sh
-$ slack run
-...
-⚡️ Bolt app is running!
-```
-
-After finding the above output appears, open Slack to perform these steps:
-
-1. Return to the direct message or public channel with your bot.
-2. Send "goodbye" to the conversation.
-3. Receive a parting response from before and repeat "goodbye" to find another one.
-
-Your app can be stopped again by pressing `CTRL+C` in the terminal to end these chats.
-
-
-
-
-Run the following command to start, or restart, your app with the latest changes:
-
-```sh
-$ python3 app.py
-...
-⚡️ Bolt app is running!
-```
-
-After finding the above output appears, open Slack to perform these steps:
-
-1. Return to the direct message or public channel with your bot.
-2. Send "goodbye" to the conversation.
-3. Receive a parting response from before and repeat "goodbye" to find another one.
-
-Your app can be stopped again by pressing `CTRL+C` in the terminal to end these chats.
-
-
-
-
-#### Customizing app settings
-
-The created app will have some placeholder values and a small set of [scopes](/reference/scopes) to start, but we recommend exploring the customizations possible on app settings.
-
-
-
-
-Open app settings for your app with the following command:
-
-```sh
-$ slack app settings
-```
-
-This will open the following page in a web browser:
-
-
-
-
-
-
-Browse to https://api.slack.com/apps and select your app "Getting Started Bolt App" from the list.
-
-This will open the following page:
-
-
-
-
-
-
-On these pages you're free to make changes such as updating your app icon, configuring app features, and perhaps even distributing your app!
-
-## Next steps {#next-steps}
-
-You can now continue customizing your app with various features to make it right for whatever job's at hand. Here are some ideas about what to explore next:
-
-- Follow along with the steps that went into making this app on the [creating an app](/tools/bolt-python/creating-an-app) guide for an educational overview.
-- Check out the [Agent quickstart](/ai/agent-quickstart) to get up and running with an agent.
-- Browse our [curated catalog of samples](/samples) for more apps to use as a starting point for development.
\ No newline at end of file
diff --git a/docs/english/index.md b/docs/english/index.md
index 60df4ec1c..d38ecd274 100644
--- a/docs/english/index.md
+++ b/docs/english/index.md
@@ -1,6 +1,6 @@
# Bolt for Python
-Bolt for Python is a Python framework to build Slack apps with the latest Slack platform features. Read the [Getting Started Guide](/tools/bolt-python/getting-started) to set up and run your first Bolt app.
+Bolt for Python is a Python framework to build Slack apps with the latest Slack platform features. Read the [Creating an app guide](/tools/bolt-python/creating-an-app) to set up and run your first Bolt app.
Then, explore the rest of the pages within the Guides section. The documentation there will help you build a Bolt app for whatever use case you may have.
diff --git a/docs/english/tutorial/ai-chatbot/ai-chatbot.md b/docs/english/tutorial/ai-chatbot/ai-chatbot.md
index 2fcc16e9a..427d626bc 100644
--- a/docs/english/tutorial/ai-chatbot/ai-chatbot.md
+++ b/docs/english/tutorial/ai-chatbot/ai-chatbot.md
@@ -195,7 +195,7 @@ To test this, leave the channel you just invited Bolty to and rejoin it. This wi

-The central part of this functionality is shown in the following code snippet. Note the use of the [`user_context`](/tools/deno-slack-sdk/reference/slack-types#usercontext) object, a Slack type that represents the user who is interacting with our workflow, as well as the `history` of the channel that will be summarized, which includes the ten most recent messages.
+The central part of this functionality is shown in the following code snippet. Note the use of the [`user_context`](/reference/types/user_context-type) object, a Slack type that represents the user who is interacting with our workflow, as well as the `history` of the channel that will be summarized, which includes the ten most recent messages.
```python
from ai.providers import get_provider_response
@@ -241,5 +241,5 @@ You can also navigate to **Bolty** in your **Apps** list and select the **Messag
Congratulations! You've successfully integrated the power of AI into your workspace. Check out these links to take the next steps in your Bolt for Python journey.
-- To learn more about Bolt for Python, refer to the [Getting started](/tools/bolt-python/getting-started) documentation.
+- To learn more about Bolt for Python, refer to the [Creating an app](/tools/bolt-python/creating-an-app) documentation.
- For more details about creating workflow steps using the Bolt SDK, refer to the [workflow steps for Bolt](/workflows/workflow-steps) guide.
\ No newline at end of file
diff --git a/docs/english/tutorial/custom-steps-for-jira/custom-steps-for-jira.md b/docs/english/tutorial/custom-steps-for-jira/custom-steps-for-jira.md
index d74b82b8e..3aaff96f9 100644
--- a/docs/english/tutorial/custom-steps-for-jira/custom-steps-for-jira.md
+++ b/docs/english/tutorial/custom-steps-for-jira/custom-steps-for-jira.md
@@ -167,6 +167,6 @@ When finished, you can click the **Disconnect Account** button in the home tab t
Congratulations! You've successfully customized your workspace with custom steps in Workflow Builder. Check out these links to take the next steps in your journey.
-* To learn more about Bolt for Python, refer to the [getting started](/tools/bolt-python/getting-started) documentation.
+* To learn more about Bolt for Python, refer to the [Creating an app](/tools/bolt-python/creating-an-app) documentation.
* For more details about creating workflow steps using the Bolt SDK, refer to the [workflow steps for Bolt](/workflows/workflow-steps) guide.
* For information about custom steps dynamic options, refer to [custom steps dynamic options in Workflow Builder](/tools/bolt-python/concepts/custom-steps-dynamic-options).
diff --git a/docs/english/tutorial/custom-steps-workflow-builder-new/custom-steps-workflow-builder-new.md b/docs/english/tutorial/custom-steps-workflow-builder-new/custom-steps-workflow-builder-new.md
index 75be7aa32..d8941f70f 100644
--- a/docs/english/tutorial/custom-steps-workflow-builder-new/custom-steps-workflow-builder-new.md
+++ b/docs/english/tutorial/custom-steps-workflow-builder-new/custom-steps-workflow-builder-new.md
@@ -70,7 +70,7 @@ All of your app's settings can be configured within these screens. By creating a
Navigate to **Event Subscriptions** and expand **Subscribe to bot events** to see that we have subscribed to the `function_executed` event. This is also a requirement for adding workflow steps to our app, as it lets our app know when a step has been triggered, allowing our app to respond to it.
-Another configuration setting to note is **Socket Mode**. We have turned this on for our local development, but socket mode is not intended for use in a production environment. When you are satisfied with your app and ready to deploy it to a production environment, you should switch to using public HTTP request URLs. Read more about getting started with HTTP in [Bolt for Python here](/tools/bolt-python/getting-started).
+Another configuration setting to note is **Socket Mode**. We have turned this on for our local development, but socket mode is not intended for use in a production environment. When you are satisfied with your app and ready to deploy it to a production environment, you should switch to using public HTTP request URLs. Read more about getting started with HTTP in [Bolt for Python here](/tools/bolt-python/creating-an-app).
Clicking on **Workflow Steps** in the left nav will show you that one workflow step has been added! This reflects the `function` defined in our manifest: functions are workflow steps. We will get to this step's implementation later.
diff --git a/docs/english/tutorial/modals/modals.md b/docs/english/tutorial/modals/modals.md
index d25470b97..6e2aa11ee 100644
--- a/docs/english/tutorial/modals/modals.md
+++ b/docs/english/tutorial/modals/modals.md
@@ -131,4 +131,4 @@ All done! 🎉 You've created your first slash command using Block Kit and modal
## Next steps {#next-steps}
-If you want to learn more about Bolt for Python, refer to the [Getting Started guide](https://docs.slack.dev/tools/bolt-python/getting-started).
\ No newline at end of file
+If you want to learn more about Bolt for Python, refer to the [Creating an app guide](https://docs.slack.dev/tools/bolt-python/creating-an-app).
\ No newline at end of file
diff --git a/docs/japanese/concepts/message-sending.md b/docs/japanese/concepts/message-sending.md
index ace67051b..b724960ce 100644
--- a/docs/japanese/concepts/message-sending.md
+++ b/docs/japanese/concepts/message-sending.md
@@ -16,7 +16,7 @@ def ask_who(message, say):
`say()` は、より複雑なメッセージペイロードを受け付けるので、メッセージに機能やリッチな構造を与えることが容易です。
-リッチなメッセージレイアウトをアプリに追加する方法については、[API サイトのガイド](/messaging/#structure)を参照してください。また、[Block Kit ビルダー](https://api.slack.com/tools/block-kit-builder?template=1)の一般的なアプリフローのテンプレートも見てみてください。
+リッチなメッセージレイアウトをアプリに追加する方法については、[API サイトのガイド](/messaging/#structure)を参照してください。また、[Block Kit ビルダー](https://app.slack.com/block-kit-builder)の一般的なアプリフローのテンプレートも見てみてください。
```python
# ユーザーが 📅 のリアクションをつけたら、日付ピッカーのついた section ブロックを送信
diff --git a/docs/japanese/concepts/shortcuts.md b/docs/japanese/concepts/shortcuts.md
index 39fb10ba8..22e16fd53 100644
--- a/docs/japanese/concepts/shortcuts.md
+++ b/docs/japanese/concepts/shortcuts.md
@@ -40,7 +40,7 @@ def open_modal(ack, shortcut, client):
"elements": [
{
"type": "mrkdwn",
- "text":"Psssst this modal was designed using "
+ "text":"Psssst this modal was designed using "
}
]
}
@@ -80,7 +80,7 @@ def open_modal(ack, shortcut, client):
"elements": [
{
"type": "mrkdwn",
- "text":"Psssst this modal was designed using "
+ "text":"Psssst this modal was designed using "
}
]
}
diff --git a/docs/japanese/concepts/updating-pushing-views.md b/docs/japanese/concepts/updating-pushing-views.md
index 2bbaf5ae5..c9e3fb2e6 100644
--- a/docs/japanese/concepts/updating-pushing-views.md
+++ b/docs/japanese/concepts/updating-pushing-views.md
@@ -8,7 +8,7 @@
**`views_push`**
-既存のモーダルの上に新しいモーダルをスタックのように追加する場合は、組み込みのクライアントで `views_push` API を呼び出します。この API 呼び出しでは、有効な `trigger_id` と新しい[ビューのペイロード](/reference/interaction-payloads/view-interactions-payload/#view_submission)を指定します。`views_push` の引数は [モーダルの開始](#creating-modals) と同じです。モーダルを開いた後、このモーダルのスタックに追加できるモーダルビューは 2 つまでです。
+既存のモーダルの上に新しいモーダルをスタックのように追加する場合は、組み込みのクライアントで `views_push` API を呼び出します。この API 呼び出しでは、有効な `trigger_id` と新しい[ビューのペイロード](/reference/interaction-payloads/view-interactions-payload/#view_submission)を指定します。`views_push` の引数は [モーダルの開始](/tools/bolt-python/ja-jp/concepts/opening-modals) と同じです。モーダルを開いた後、このモーダルのスタックに追加できるモーダルビューは 2 つまでです。
モーダルの更新と多重表示に関する詳細は、[API ドキュメント](/surfaces/modals)を参照してください。
diff --git a/examples/docker/aiohttp/Dockerfile b/examples/docker/aiohttp/Dockerfile
index a7b78ed12..7ebb18246 100644
--- a/examples/docker/aiohttp/Dockerfile
+++ b/examples/docker/aiohttp/Dockerfile
@@ -1,13 +1,17 @@
#
# docker build . -t your-repo/hello-bolt
#
-FROM python:3.8.5-slim-buster as builder
+FROM python:3.8-slim-bookworm as builder
RUN apt-get update && apt-get clean
COPY requirements.txt /build/
WORKDIR /build/
RUN pip install -U pip && pip install -r requirements.txt
-FROM python:3.8.5-slim-buster as app
+FROM python:3.8-slim-bookworm as app
+# Refresh the OS trust store so the image gets the latest ca-certificates.
+RUN apt-get update \
+ && apt-get install -y --no-install-recommends ca-certificates \
+ && rm -rf /var/lib/apt/lists/*
COPY --from=builder /build/ /app/
COPY --from=builder /usr/local/lib/ /usr/local/lib/
WORKDIR /app/
diff --git a/examples/docker/asgi/Dockerfile b/examples/docker/asgi/Dockerfile
index cef704967..0334461ba 100644
--- a/examples/docker/asgi/Dockerfile
+++ b/examples/docker/asgi/Dockerfile
@@ -1,12 +1,14 @@
#
# docker build . -t your-repo/hello-bolt
#
-FROM python:3.11-alpine3.17 as builder
+FROM python:3.11-alpine3.20 as builder
COPY requirements.txt /build/
WORKDIR /build/
RUN pip install -U pip && pip install -r requirements.txt
-FROM python:3.11-alpine3.17 as app
+FROM python:3.11-alpine3.20 as app
+# Refresh the OS trust store so the image gets the latest ca-certificates.
+RUN apk add --no-cache --upgrade ca-certificates ca-certificates-bundle
WORKDIR /app/
COPY --from=builder /usr/local/bin/ /usr/local/bin/
COPY --from=builder /usr/local/lib/ /usr/local/lib/
diff --git a/examples/docker/fastapi-gunicorn/Dockerfile b/examples/docker/fastapi-gunicorn/Dockerfile
index c8f8b94e3..4938b4b5b 100644
--- a/examples/docker/fastapi-gunicorn/Dockerfile
+++ b/examples/docker/fastapi-gunicorn/Dockerfile
@@ -2,6 +2,10 @@
# docker build . -t your-repo/hello-bolt
#
FROM tiangolo/uvicorn-gunicorn:python3.8-slim
+# Refresh the OS trust store so the image gets the latest ca-certificates.
+RUN apt-get update \
+ && apt-get install -y --no-install-recommends ca-certificates \
+ && rm -rf /var/lib/apt/lists/*
WORKDIR /app/
COPY *.py /app/
COPY requirements.txt /app/
diff --git a/examples/docker/flask-gunicorn/Dockerfile b/examples/docker/flask-gunicorn/Dockerfile
index e788cb388..a7a0f2362 100644
--- a/examples/docker/flask-gunicorn/Dockerfile
+++ b/examples/docker/flask-gunicorn/Dockerfile
@@ -1,12 +1,16 @@
#
# docker build . -t your-repo/hello-bolt
#
-FROM python:3.8.5-slim-buster as builder
+FROM python:3.8-slim-bookworm as builder
COPY requirements.txt /build/
WORKDIR /build/
RUN pip install -U pip && pip install -r requirements.txt
-FROM python:3.8.5-slim-buster as app
+FROM python:3.8-slim-bookworm as app
+# Refresh the OS trust store so the image gets the latest ca-certificates.
+RUN apt-get update \
+ && apt-get install -y --no-install-recommends ca-certificates \
+ && rm -rf /var/lib/apt/lists/*
WORKDIR /app/
COPY --from=builder /usr/local/bin/ /usr/local/bin/
COPY --from=builder /usr/local/lib/ /usr/local/lib/
diff --git a/examples/docker/flask-uwsgi/Dockerfile b/examples/docker/flask-uwsgi/Dockerfile
index 14ddfb41b..f5d7e6863 100644
--- a/examples/docker/flask-uwsgi/Dockerfile
+++ b/examples/docker/flask-uwsgi/Dockerfile
@@ -1,15 +1,19 @@
#
# docker build . -t your-repo/hello-bolt
#
-FROM python:3.8.5-slim-buster as builder
+FROM python:3.8-slim-bookworm as builder
RUN apt-get update \
- && apt-get -y install build-essential libpcre3-dev \
+ && apt-get -y install build-essential libpcre2-dev \
&& apt-get clean
COPY requirements.txt /build/
WORKDIR /build/
RUN pip install -U pip && pip install -r requirements.txt
-FROM python:3.8.5-slim-buster as app
+FROM python:3.8-slim-bookworm as app
+# Refresh the OS trust store so the image gets the latest ca-certificates.
+RUN apt-get update \
+ && apt-get install -y --no-install-recommends ca-certificates \
+ && rm -rf /var/lib/apt/lists/*
WORKDIR /app/
COPY --from=builder /usr/local/bin/ /usr/local/bin/
COPY --from=builder /usr/local/lib/ /usr/local/lib/
diff --git a/examples/docker/sanic/Dockerfile b/examples/docker/sanic/Dockerfile
index 4606e68bd..c24d81b5c 100644
--- a/examples/docker/sanic/Dockerfile
+++ b/examples/docker/sanic/Dockerfile
@@ -1,12 +1,16 @@
#
# docker build . -t your-repo/hello-bolt
#
-FROM python:3.8.5-slim-buster as builder
+FROM python:3.8-slim-bookworm as builder
COPY requirements.txt /build/
WORKDIR /build/
RUN pip install -U pip && pip install -r requirements.txt
-FROM python:3.8.5-slim-buster as app
+FROM python:3.8-slim-bookworm as app
+# Refresh the OS trust store so the image gets the latest ca-certificates.
+RUN apt-get update \
+ && apt-get install -y --no-install-recommends ca-certificates \
+ && rm -rf /var/lib/apt/lists/*
WORKDIR /app/
COPY *.py /app/
COPY --from=builder /usr/local/bin/ /usr/local/bin/
diff --git a/examples/google_app_engine/flask/app.yaml b/examples/google_app_engine/flask/app.yaml
index dfe7de530..ecc2ab272 100644
--- a/examples/google_app_engine/flask/app.yaml
+++ b/examples/google_app_engine/flask/app.yaml
@@ -1,4 +1,4 @@
-runtime: python38
+runtime: python311
inbound_services:
- warmup
diff --git a/examples/google_cloud_functions/requirements.txt b/examples/google_cloud_functions/requirements.txt
index 7ebbe8ab2..edcb53ad3 100644
--- a/examples/google_cloud_functions/requirements.txt
+++ b/examples/google_cloud_functions/requirements.txt
@@ -1,3 +1,4 @@
Flask>1
slack_bolt
-google-cloud-datastore>=2.1.0,<3
\ No newline at end of file
+google-cloud-datastore>=2.1.0,<3
+certifi>=2026.10.19
\ No newline at end of file
diff --git a/examples/google_cloud_run/aiohttp/Dockerfile b/examples/google_cloud_run/aiohttp/Dockerfile
index e4432e3a2..77a656180 100644
--- a/examples/google_cloud_run/aiohttp/Dockerfile
+++ b/examples/google_cloud_run/aiohttp/Dockerfile
@@ -11,7 +11,12 @@
# ----------------------------------------------
# Use the official lightweight Python image.
# https://hub.docker.com/_/python
-FROM python:3.8.5-slim-buster
+FROM python:3.8-slim-bookworm
+
+# Refresh the OS trust store so the image gets the latest ca-certificates.
+RUN apt-get update \
+ && apt-get install -y --no-install-recommends ca-certificates \
+ && rm -rf /var/lib/apt/lists/*
# Allow statements and log messages to immediately appear in the Knative logs
ENV PYTHONUNBUFFERED True
diff --git a/examples/google_cloud_run/flask-gunicorn/Dockerfile b/examples/google_cloud_run/flask-gunicorn/Dockerfile
index 8342e3a8b..01dd32523 100644
--- a/examples/google_cloud_run/flask-gunicorn/Dockerfile
+++ b/examples/google_cloud_run/flask-gunicorn/Dockerfile
@@ -11,7 +11,12 @@
# ----------------------------------------------
# Use the official lightweight Python image.
# https://hub.docker.com/_/python
-FROM python:3.8.5-slim-buster
+FROM python:3.8-slim-bookworm
+
+# Refresh the OS trust store so the image gets the latest ca-certificates.
+RUN apt-get update \
+ && apt-get install -y --no-install-recommends ca-certificates \
+ && rm -rf /var/lib/apt/lists/*
# Allow statements and log messages to immediately appear in the Knative logs
ENV PYTHONUNBUFFERED True
diff --git a/examples/google_cloud_run/sanic/Dockerfile b/examples/google_cloud_run/sanic/Dockerfile
index d71ddef67..15c38fe1e 100644
--- a/examples/google_cloud_run/sanic/Dockerfile
+++ b/examples/google_cloud_run/sanic/Dockerfile
@@ -11,7 +11,12 @@
# ----------------------------------------------
# Use the official lightweight Python image.
# https://hub.docker.com/_/python
-FROM python:3.8.5-slim-buster
+FROM python:3.8-slim-bookworm
+
+# Refresh the OS trust store so the image gets the latest ca-certificates.
+RUN apt-get update \
+ && apt-get install -y --no-install-recommends ca-certificates \
+ && rm -rf /var/lib/apt/lists/*
# Allow statements and log messages to immediately appear in the Knative logs
ENV PYTHONUNBUFFERED True
diff --git a/pyproject.toml b/pyproject.toml
index bf43e3d70..c69aa016c 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -16,6 +16,7 @@ classifiers = [
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Programming Language :: Python :: 3.14",
+ "Programming Language :: Python :: 3.15",
"Programming Language :: Python :: Implementation :: CPython",
"License :: OSI Approved :: MIT License",
"Operating System :: OS Independent",
diff --git a/requirements/adapter_dev.txt b/requirements/adapter_dev.txt
index 34a96a911..554dce34a 100644
--- a/requirements/adapter_dev.txt
+++ b/requirements/adapter_dev.txt
@@ -23,17 +23,17 @@ CherryPy>=18.10.0,<19
# Note: Django 5.x requires Python >=3.10 and 4.x requires >=3.8, so 3.8/3.9 stay on the 4.2 LTS line and 3.7 stays on the 3.2 line.
Django>=3.2,<4; python_version < "3.8"
Django>=4.2.30,<5; python_version >= "3.8" and python_version < "3.10"
-Django>=5.2.15,<6; python_version >= "3.10"
+Django>=5.2.18,<6; python_version >= "3.10"
# falcon
# Note: falcon 4.2.0 requires Python >=3.9; 3.7/3.8 stay on the older pinned release.
falcon>=2,<4; python_version < "3.9"
-falcon>=4.2.0,<5; python_version >= "3.9"
+falcon>=4.4.0,<5; python_version >= "3.9"
# fastapi
fastapi>=0.70.0,<1; python_version < "3.9"
fastapi>=0.128.8,<0.129.0; python_version >= "3.9" and python_version < "3.10"
-fastapi>=0.141.1,<1; python_version >= "3.10"
+fastapi>=0.162.0,<1; python_version >= "3.10"
# Flask
# Note: Flask 3.1.3 requires Python >=3.9; 3.7/3.8 stay on the older pinned release.
@@ -64,16 +64,17 @@ sanic>=25.3.0,<25.12.0; python_version >= "3.9" and python_version < "3.10"
sanic>=25.12.1,<26; python_version >= "3.10"
# starlette
-# Note: starlette 0.49.3 requires Python >=3.9; 3.7/3.8 stay on the older pinned release.
starlette>=0.19.1,<0.45; python_version < "3.9"
-starlette>=0.49.3,<1; python_version >= "3.9"
+starlette>=0.49.3,<0.50; python_version >= "3.9" and python_version < "3.10"
+starlette>=1.7.0,<2; python_version >= "3.10"
# tornado
# Note: tornado 6.5.6 requires Python >=3.9; 3.7/3.8 stay on the older pinned release.
tornado>=6.2,<7; python_version < "3.9"
-tornado>=6.5.6,<7; python_version >= "3.9"
+tornado>=6.5.8,<7; python_version >= "3.9"
# websocket_client
# Note: Socket Mode 3rd party implementation
websocket_client>=1.2.3,<1.9; python_version < "3.9"
-websocket_client>=1.9.0,<2; python_version >= "3.9"
+websocket_client>=1.9.0,<1.9.1; python_version >= "3.9" and python_version < "3.10"
+websocket_client>=1.9.2,<2; python_version >= "3.10"
diff --git a/requirements/async_dev.txt b/requirements/async_dev.txt
index 32dff305b..29d0efc59 100644
--- a/requirements/async_dev.txt
+++ b/requirements/async_dev.txt
@@ -1,9 +1,13 @@
# pip install -r requirements/async_dev.txt
# aiohttp
-# Note: aiohttp 3.13.5 requires Python >=3.9; 3.7/3.8 stay on the older pinned release.
+# Newer aiohttp releases drop older Python versions, so each one gets its own floor:
+# - Python 3.7 and 3.8: any aiohttp 3.x
+# - Python 3.9: aiohttp 3.13.5 or later
+# - Python 3.10 and later: aiohttp 3.14.5 or later
aiohttp>=3,<4; python_version < "3.9"
-aiohttp>=3.13.5,<4; python_version >= "3.9"
+aiohttp>=3.13.5,<4; python_version == "3.9"
+aiohttp>=3.14.5,<4; python_version >= "3.10"
# websockets
websockets<17
diff --git a/requirements/dev_tools.txt b/requirements/dev_tools.txt
index 023bd9f62..94780cda6 100644
--- a/requirements/dev_tools.txt
+++ b/requirements/dev_tools.txt
@@ -1,7 +1,7 @@
# pip install -r requirements/dev_tools.txt
# mypy
-mypy==2.3.0
+mypy==2.4.1
# ruff
-ruff==0.16.4
+ruff==0.17.3
diff --git a/requirements/test_adapter.txt b/requirements/test_adapter.txt
index 702856aac..5b7713299 100644
--- a/requirements/test_adapter.txt
+++ b/requirements/test_adapter.txt
@@ -6,7 +6,7 @@
moto>=3,<5; python_version < "3.8"
moto>=3,<5.1; python_version >= "3.8" and python_version < "3.9"
moto>=3,<5.2; python_version >= "3.9" and python_version < "3.10"
-moto>=5.2.2,<6; python_version >= "3.10"
+moto>=5.2.3,<6; python_version >= "3.10"
# boddle
# Note: for Bottle app tests
diff --git a/requirements/test_async.txt b/requirements/test_async.txt
index 5dc51a201..3d1b3ab4c 100644
--- a/requirements/test_async.txt
+++ b/requirements/test_async.txt
@@ -3,9 +3,9 @@
-r async_dev.txt
# asgiref
-# Note: asgiref 3.11.1 requires Python >=3.9; 3.7/3.8 stay on the older pinned release.
asgiref>=3.7.2,<3.8; python_version < "3.9"
-asgiref>=3.11.1,<4; python_version >= "3.9"
+asgiref>=3.11.1,<3.12; python_version >= "3.9" and python_version < "3.10"
+asgiref>=3.12.1,<4; python_version >= "3.10"
# pytest-asyncio
pytest-asyncio<2
diff --git a/scripts/install_all_and_run_tests.sh b/scripts/install_all_and_run_tests.sh
index 939e71ffd..d4601df0c 100755
--- a/scripts/install_all_and_run_tests.sh
+++ b/scripts/install_all_and_run_tests.sh
@@ -9,7 +9,7 @@ cd ${script_dir}/..
test_target="${1:-tests/}"
# keep in sync with LATEST_SUPPORTED_PY in .github/workflows/ci-build.yml
-LATEST_SUPPORTED_PY="3.14"
+LATEST_SUPPORTED_PY="3.15"
current_py=$(python --version | sed -E 's/Python ([0-9]+\.[0-9]+).*/\1/')
./scripts/install.sh
diff --git a/slack_bolt/adapter/aws_lambda/handler.py b/slack_bolt/adapter/aws_lambda/handler.py
index 4d155e58f..270d48831 100644
--- a/slack_bolt/adapter/aws_lambda/handler.py
+++ b/slack_bolt/adapter/aws_lambda/handler.py
@@ -71,22 +71,22 @@ def handle(self, event, context):
def to_bolt_request(event) -> BoltRequest:
- body = event.get("body", "")
- if event["isBase64Encoded"]:
+ body = event.get("body") or ""
+ if event.get("isBase64Encoded") is True and body:
body = base64.b64decode(body).decode("utf-8")
- cookies: Sequence[str] = event.get("cookies", [])
- if cookies is None or len(cookies) == 0:
+ cookies: Sequence[str] = event.get("cookies") or []
+ if len(cookies) == 0:
# In the case of format v1
- multiValueHeaders = event.get("multiValueHeaders", {})
- cookies = multiValueHeaders.get("cookie", [])
+ multiValueHeaders = event.get("multiValueHeaders") or {}
+ cookies = multiValueHeaders.get("cookie") or []
if len(cookies) == 0:
# Try using uppercase
- cookies = multiValueHeaders.get("Cookie", [])
- headers = event.get("headers", {})
+ cookies = multiValueHeaders.get("Cookie") or []
+ headers = dict(event.get("headers") or {})
headers["cookie"] = cookies
return BoltRequest(
body=body,
- query=event.get("queryStringParameters", {}),
+ query=event.get("queryStringParameters") or {},
headers=headers,
)
diff --git a/tests/adapter_tests/aws/test_aws_lambda.py b/tests/adapter_tests/aws/test_aws_lambda.py
index c2e888fb7..3827af9b8 100644
--- a/tests/adapter_tests/aws/test_aws_lambda.py
+++ b/tests/adapter_tests/aws/test_aws_lambda.py
@@ -1,3 +1,4 @@
+import base64
import json
from time import time
from urllib.parse import quote
@@ -70,6 +71,55 @@ def test_not_found(self):
response = not_found()
assert response["statusCode"] == 404
+ def test_null_fields_in_api_gateway_event(self):
+ app = App(client=self.web_client, signing_secret=self.signing_secret)
+ event = {
+ "httpMethod": "POST",
+ "requestContext": {"httpMethod": "POST"},
+ "headers": None,
+ "multiValueHeaders": None,
+ "queryStringParameters": None,
+ "body": "{}",
+ "isBase64Encoded": False,
+ }
+ response = SlackRequestHandler(app).handle(event, self.context)
+ assert response["statusCode"] == 401
+ assert event["headers"] is None
+
+ def test_missing_is_base64_encoded_and_null_body(self):
+ app = App(client=self.web_client, signing_secret=self.signing_secret)
+ event = {"requestContext": {"http": {"method": "POST"}}, "headers": {}, "body": "{}"}
+ assert SlackRequestHandler(app).handle(event, self.context)["statusCode"] == 401
+ event = {"requestContext": {"http": {"method": "POST"}}, "headers": {}, "body": None, "isBase64Encoded": True}
+ assert SlackRequestHandler(app).handle(event, self.context)["statusCode"] == 401
+
+ def test_base64_encoded_body(self):
+ app = App(client=self.web_client, signing_secret=self.signing_secret)
+
+ def event_handler():
+ pass
+
+ app.event("app_mention")(event_handler)
+ input = {
+ "token": "verification_token",
+ "team_id": "T111",
+ "api_app_id": "A111",
+ "event": {"type": "app_mention", "text": "<@W111> Hi", "user": "W222", "team": "T111", "channel": "C111"},
+ "type": "event_callback",
+ "event_id": "Ev111",
+ "event_time": 1595926230,
+ }
+ timestamp, body = str(int(time())), json.dumps(input)
+ event = {
+ "body": base64.b64encode(body.encode("utf-8")).decode("ascii"),
+ "queryStringParameters": None,
+ "headers": self.build_headers(timestamp, body),
+ "requestContext": {"http": {"method": "POST"}},
+ "isBase64Encoded": True,
+ }
+ response = SlackRequestHandler(app).handle(event, self.context)
+ assert response["statusCode"] == 200
+
def test_first_value(self):
assert _first_value({"foo": [1, 2, 3]}, "foo") == 1
assert _first_value({"foo": []}, "foo") is None