diff --git a/.claude/.gitignore b/.claude/.gitignore new file mode 100644 index 000000000..3a2f7f6a1 --- /dev/null +++ b/.claude/.gitignore @@ -0,0 +1,4 @@ +CLAUDE.local.md +settings.local.json +worktrees/ +plans/ diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md new file mode 100644 index 000000000..dba71e970 --- /dev/null +++ b/.claude/CLAUDE.md @@ -0,0 +1 @@ +@../AGENTS.md diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 000000000..4006f7bd9 --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,36 @@ +{ + "permissions": { + "allow": [ + "Bash(./scripts/build_pypi_package.sh:*)", + "Bash(./scripts/format.sh:*)", + "Bash(./scripts/generate_api_docs.sh:*)", + "Bash(./scripts/install.sh:*)", + "Bash(./scripts/lint.sh:*)", + "Bash(./scripts/run_mypy.sh:*)", + "Bash(./scripts/run_tests.sh:*)", + "Bash(./scripts/run_validation.sh:*)", + "Bash(./scripts/uninstall_all.sh:*)", + "Bash(python scripts/codegen.py:*)", + "Bash(echo $VIRTUAL_ENV)", + "Bash(gh issue view:*)", + "Bash(gh label list:*)", + "Bash(gh pr checks:*)", + "Bash(gh pr diff:*)", + "Bash(gh pr list:*)", + "Bash(gh pr status:*)", + "Bash(gh pr update-branch:*)", + "Bash(gh pr view:*)", + "Bash(gh search code:*)", + "Bash(git diff:*)", + "Bash(git grep:*)", + "Bash(git log:*)", + "Bash(git show:*)", + "Bash(git status:*)", + "Bash(grep:*)", + "Bash(ls:*)", + "Bash(tree:*)", + "WebFetch(domain:github.com)", + "WebFetch(domain:docs.slack.dev)" + ] + } +} diff --git a/.github/maintainers_guide.md b/.github/maintainers_guide.md index ccda1607f..27927530d 100644 --- a/.github/maintainers_guide.md +++ b/.github/maintainers_guide.md @@ -88,13 +88,13 @@ Run all the unit tests, code linter, and code analyzer: Run all the unit tests (no linter nor code analyzer): ```sh -./scripts/run_unit_tests.sh +./scripts/run_tests.sh ``` Run a specific unit test: ```sh -./scripts/run_unit_tests.sh tests/web/test_web_client.py +./scripts/run_tests.sh tests/web/test_web_client.py ``` You can rely on GitHub Actions builds for running the tests on a variety of Python runtimes. diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 000000000..3f59d5c0a --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,308 @@ +# AGENTS.md — Python Slack SDK + +## Project Overview + +The Python Slack SDK (`slack_sdk`) is a modular Python library for interacting with the Slack platform APIs. It is published on PyPI as `slack-sdk`. The SDK provides independent packages for each Slack API surface: Web API, Webhooks, Socket Mode, OAuth, Audit Logs, SCIM, RTM, Block Kit models, and request signature verification. + +- **Repository**: +- **Documentation**: +- **PyPI**: +- **Current version**: defined in `slack_sdk/version.py` + +## Critical Rules + +These are the most important constraints in this project. Violating any of them will break CI or corrupt auto-generated code: + +1. **Never edit auto-generated files.** The following files are produced by `scripts/codegen.py` and must not be modified directly: + - `slack_sdk/web/async_client.py` + - `slack_sdk/web/legacy_client.py` + - `slack_sdk/web/async_chat_stream.py` + + Edit the source files (`client.py` or `chat_stream.py`) instead, then run codegen (see [Code Generation](#code-generation-critical-pattern)). + +2. **Zero runtime dependencies.** The core sync Web API client must have no required runtime dependencies. Do not add entries to `install_requires` / `dependencies` in `pyproject.toml`. + +3. **Do not modify the legacy `slack/` package.** It is in maintenance mode and only re-exports from `slack_sdk` with deprecation warnings. All new development goes in `slack_sdk/`. + +4. **Always run codegen + format after editing `client.py` or `chat_stream.py`:** + + ```sh + python scripts/codegen.py --path . + ./scripts/format.sh + ``` + +5. **Use project scripts, not raw tool commands.** Run `./scripts/run_tests.sh`, not `pytest` directly. The scripts handle codegen and formatting. + +## Architecture + +### Package Structure + +The SDK is organized into independent sub-packages: + +- **`slack_sdk/web/`** — Web API client (sync, async, legacy). Contains auto-generated files (see [Code Generation](#code-generation-critical-pattern)) +- **`slack_sdk/webhook/`** — Incoming Webhooks +- **`slack_sdk/socket_mode/`** — Socket Mode with pluggable backends +- **`slack_sdk/oauth/`** — OAuth flows and token storage +- **`slack_sdk/models/`** — Block Kit UI builders +- **`slack_sdk/audit_logs/`**, **`slack_sdk/scim/`** — Enterprise APIs +- **`slack_sdk/signature/`** — Request verification +- **`slack_sdk/http_retry/`** — Retry handlers +- **`slack/`** — Legacy package (maintenance mode, do not modify) + +See the repository structure for the complete package layout. + +### Code Generation (Critical Pattern) + +**NEVER edit these auto-generated files:** + +- `slack_sdk/web/async_client.py` +- `slack_sdk/web/legacy_client.py` +- `slack_sdk/web/async_chat_stream.py` + +Each contains a header warning: +```text +# DO NOT EDIT THIS FILE +# 1) Modify slack_sdk/web/client.py +# 2) Run `python scripts/codegen.py` +# 3) Run `black slack_sdk/` +``` + +**How it works:** + +1. Edit `slack_sdk/web/client.py` (canonical source for Web API methods) +2. Edit `slack_sdk/web/chat_stream.py` (canonical source for streaming chat) +3. Run `python scripts/codegen.py --path .` to generate async/legacy variants +4. Run `./scripts/format.sh` to format the generated code + +The codegen script (`scripts/codegen.py`) automatically transforms sync code into async variants by adding `async def`, `await`, and replacing classes with async equivalents. + +### Web API Method Pattern + +Every Web API method in `client.py` follows this pattern: + +```python +def method_name( + self, + *, # keyword-only arguments + required_param: str, + optional_param: Optional[str] = None, + **kwargs, +) -> SlackResponse: + """Description of the API method + https://docs.slack.dev/reference/methods/method.name + """ + kwargs.update({"required_param": required_param}) + if optional_param is not None: + kwargs.update({"optional_param": optional_param}) + return self.api_call("method.name", params=kwargs) +``` + +Key conventions: + +- All parameters are keyword-only (after `*`) +- Required params have no default; optional params default to `None` +- `**kwargs` captures additional/undocumented parameters +- Parameters are collected into `kwargs` dict and passed to `self.api_call()` +- The `api_call` method name uses Slack's dot-notation (e.g., `"chat.postMessage"`) +- Docstrings include a link to the Slack API reference + +### Error Types + +All SDK exceptions are defined in `slack_sdk/errors/__init__.py` and inherit from `SlackClientError`. + +Key exceptions to be aware of: + +- **`SlackApiError`** — Raised when the API returns an error response (carries the `response` object) +- **`SlackRequestError`** — Raised when the HTTP request itself fails +- **`BotUserAccessError`** — Raised when using a bot token (`xoxb-*`) for a user-only method + +See `slack_sdk/errors/__init__.py` for the complete list of exception types and their usage. + +### HTTP Retry Handlers + +The `slack_sdk/http_retry/` module provides built-in retry strategies for connection errors, rate limiting (HTTP 429), and server errors (HTTP 500/503). + +Key handlers: + +- **`ConnectionErrorRetryHandler`** / **`AsyncConnectionErrorRetryHandler`** +- **`RateLimitErrorRetryHandler`** / **`AsyncRateLimitErrorRetryHandler`** (respects `Retry-After` header) +- **`ServerErrorRetryHandler`** / **`AsyncServerErrorRetryHandler`** + +Retry intervals can be configured with `BackoffRetryIntervalCalculator` (exponential backoff) or `FixedValueRetryIntervalCalculator`. See `slack_sdk/http_retry/` for implementation details. + +### Test Patterns + +Tests use `unittest.TestCase` with a mock web API server: + +```python +import unittest +from slack_sdk import WebClient +from tests.slack_sdk.web.mock_web_api_handler import MockHandler +from tests.mock_web_api_server import setup_mock_web_api_server, cleanup_mock_web_api_server + +class TestFeature(unittest.TestCase): + def setUp(self): + setup_mock_web_api_server(self, MockHandler) + self.client = WebClient( + token="xoxb-api_test", + base_url="http://localhost:8888", + ) + + def tearDown(self): + cleanup_mock_web_api_server(self) + + def test_something(self): + resp = self.client.api_test() + self.assertTrue(resp["ok"]) +``` + +Each sub-package has its own `MockHandler` (e.g., `tests/slack_sdk/webhook/mock_web_api_handler.py`, `tests/slack_sdk/scim/mock_web_api_handler.py`). Use the handler from the matching sub-package. + +Test directories: + +- `tests/` — Unit tests (mirroring `slack_sdk/` structure) + - `tests/slack_sdk/` — Sync tests + - `tests/slack_sdk_async/` — Async variants + - `tests/slack_sdk_fixture/` — Pytest fixtures and test data + - `tests/data/` — JSON fixture files + - `tests/mock_web_api_server/` — Mock Slack API server +- `integration_tests/` — Tests against real Slack APIs (require env tokens) + +## Development Commands + +### Setup + +**Prerequisites:** A Python virtual environment must be activated. See `.github/maintainers_guide.md` for detailed setup instructions using `pyenv` and `venv`. + +**Quick check:** Verify venv is active with `echo $VIRTUAL_ENV` (should output a path). + +Always use the project scripts instead of calling tools like `pytest` directly. + +### Install Dependencies + +```sh +./scripts/install.sh +``` + +Installs all project dependencies (testing, optional, and tools) via pip. This script is called automatically by `run_validation.sh`, `run_integration_tests.sh`, and `run_mypy.sh`, so you typically don't need to run it separately. + +### Full Validation + +```sh +./scripts/run_validation.sh +``` + +This is the canonical check — CI runs this and the PR template asks contributors to run it. It installs requirements, runs codegen, formats, lints, runs tests with coverage, and runs mypy type checking on the latest supported Python version (check the `LATEST_SUPPORTED_PY` environment variable in `.github/workflows/ci-build.yml`). + +### Individual Commands + +Available scripts in the `scripts/` directory: + +| Task | Command | +| --- | --- | +| Install dependencies | `./scripts/install.sh` | +| Uninstall all packages | `./scripts/uninstall_all.sh` | +| Format code | `./scripts/format.sh` | +| Lint (check formatting) | `./scripts/lint.sh` | +| Run all unit tests | `./scripts/run_tests.sh` | +| Run a specific test | `./scripts/run_tests.sh tests/slack_sdk/web/test_web_client.py` | +| Run type checking | `./scripts/run_mypy.sh` | +| Generate async/legacy code | `python scripts/codegen.py --path .` | +| Build PyPI package | `./scripts/build_pypi_package.sh` | +| Generate API docs | `./scripts/generate_api_docs.sh` | + +## Code Style & Tooling + +All tooling configuration is defined in the following files: + +- **Formatter**: `black` — see `[tool.black]` in `pyproject.toml` +- **Linter**: `flake8` — see `.flake8` +- **Type checker**: `mypy` — see `[tool.mypy]` in `pyproject.toml` +- **Test runner**: `pytest` — see `[tool.pytest.ini_options]` in `pyproject.toml` +- **Coverage**: `pytest-cov` reporting to Codecov +- **Build system**: see `[build-system]` and `[project]` in `pyproject.toml` + +**Dependencies:** + +- Testing: `requirements/testing.txt` +- Optional runtime: `requirements/optional.txt` +- Dev tools (black, flake8, mypy): `requirements/tools.txt` + +## CI Pipeline (GitHub Actions) + +Defined in `.github/workflows/ci-build.yml`, runs on push to `main`, all PRs, and daily schedule. Check the workflow file for the current Python version matrix. + +## Key Files & Directories + +**Source Code:** + +- `slack_sdk/` — Main package (active development) +- `slack_sdk/web/client.py` — **CANONICAL SOURCE** for all Web API methods +- `slack_sdk/web/chat_stream.py` — Canonical streaming chat client +- `slack_sdk/version.py` — Single source of truth for version +- `slack/` — Legacy package (maintenance mode, DO NOT MODIFY) + +**Configuration:** + +- `pyproject.toml` — Project metadata, build config, tool settings (black, pytest, mypy) +- `.flake8` — Flake8 linter configuration +- `requirements/*.txt` — Dependency specifications + +**Tooling:** + +- `scripts/codegen.py` — Generates async/legacy client variants +- `scripts/*.sh` — Development and CI helper scripts + +**GitHub & CI/CD:** + +- `.github/` — GitHub-specific configuration and documentation +- `.github/workflows/` — Continuous integration pipeline definitions that run on GitHub Actions +- `.github/maintainers_guide.md` — Maintainer workflows and release process + +**Documentation:** + +- `README.md` — Project overview, installation, and usage examples +- `.github/maintainers_guide.md` — Maintainer workflows and release process + +## Common Contribution Workflows + +### Adding a New Web API Method + +1. Add the method to `slack_sdk/web/client.py` following the existing pattern +2. Run code generation: `python scripts/codegen.py --path .` +3. Run formatter: `./scripts/format.sh` +4. Add tests in `tests/slack_sdk/web/` +5. Validate: `./scripts/run_validation.sh` + +### Adding a New Feature to a Non-Web Module + +1. Implement the sync version in the appropriate `slack_sdk/` subpackage +2. If the module has async variants, implement those as well (not auto-generated for non-web modules) +3. Add tests mirroring the module structure +4. Validate: `./scripts/run_validation.sh` + +### Fixing a Bug + +1. Write a test that reproduces the bug +2. Fix the code (if in `client.py`, run codegen afterward) +3. Validate: `./scripts/run_validation.sh` + +## Versioning & Releases + +- Use the new version mentioned by the maintainer; if they do not provide it, prompt them for it. +- Ensure the new version follows [Semantic Versioning](http://semver.org/) via [PEP 440](https://peps.python.org/pep-0440/) +- Version lives in `slack_sdk/version.py` and is dynamically read by `pyproject.toml` +- Releases are triggered by publishing a GitHub Release, which triggers the PyPI deployment workflow; the maintainer will take care of this +- Commit message format for releases: `chore(release): version X.Y.Z` + +## Dependencies + +The SDK has **zero required runtime dependencies** for the core sync Web API client and it is imperative it remains that way. Optional dependencies enable additional functionality: + +- `aiohttp` — async HTTP client +- `websockets` / `websocket-client` — Socket Mode backends +- `SQLAlchemy` — OAuth token storage +- `boto3` — S3/DynamoDB token storage +- `aiodns` — faster DNS resolution for async + +Version constraints for optional and dev dependencies are pinned in the `requirements/` directory. diff --git a/scripts/install.sh b/scripts/install.sh new file mode 100755 index 000000000..32a3204e8 --- /dev/null +++ b/scripts/install.sh @@ -0,0 +1,14 @@ +#!/bin/bash +# ./scripts/install.sh +# Installs all project dependencies (testing, optional, and tools) + +set -e + +script_dir=$(dirname $0) +cd ${script_dir}/.. + +pip install -U pip + +pip install -U -r requirements/testing.txt \ + -U -r requirements/optional.txt \ + -U -r requirements/tools.txt diff --git a/scripts/run_integration_tests.sh b/scripts/run_integration_tests.sh index 1a6f254cb..7fa598a9b 100755 --- a/scripts/run_integration_tests.sh +++ b/scripts/run_integration_tests.sh @@ -8,10 +8,7 @@ set -e script_dir=`dirname $0` cd ${script_dir}/.. -pip install -U pip -pip install -U -r requirements/testing.txt \ - -U -r requirements/optional.txt \ - -U -r requirements/tools.txt +./scripts/install.sh echo "Generating code ..." && python scripts/codegen.py --path . echo "Running black (code formatter) ..." && ./scripts/format.sh --no-install diff --git a/scripts/run_mypy.sh b/scripts/run_mypy.sh index cc1146f15..e6452d40e 100755 --- a/scripts/run_mypy.sh +++ b/scripts/run_mypy.sh @@ -6,9 +6,8 @@ set -e script_dir=$(dirname $0) cd ${script_dir}/.. -pip install -U pip setuptools wheel -pip install -U -r requirements/testing.txt \ - -U -r requirements/optional.txt \ - -U -r requirements/tools.txt +if [[ "$1" != "--no-install" ]]; then + ./scripts/install.sh +fi mypy --config-file pyproject.toml diff --git a/scripts/run_unit_tests.sh b/scripts/run_tests.sh similarity index 57% rename from scripts/run_unit_tests.sh rename to scripts/run_tests.sh index c8ab0af78..898be91a7 100755 --- a/scripts/run_unit_tests.sh +++ b/scripts/run_tests.sh @@ -1,21 +1,17 @@ #!/bin/bash # Run all the tests or a single test -# all: ./scripts/run_unit_tests.sh -# single: ./scripts/run_unit_tests.sh tests/slack_sdk_async/web/test_web_client_coverage.py +# all: ./scripts/run_tests.sh +# single: ./scripts/run_tests.sh tests/slack_sdk_async/web/test_web_client_coverage.py set -e script_dir=`dirname $0` cd ${script_dir}/.. -pip install -U pip -pip install -U -r requirements/testing.txt \ - -U -r requirements/optional.txt \ - -U -r requirements/tools.txt +test_target="${1:-tests/}" echo "Generating code ..." && python scripts/codegen.py --path . 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 366f0d321..db6eef6f9 100755 --- a/scripts/run_validation.sh +++ b/scripts/run_validation.sh @@ -7,9 +7,11 @@ set -e script_dir=`dirname $0` cd ${script_dir}/.. -pip install -U -r requirements/testing.txt \ - -U -r requirements/optional.txt \ - -U -r requirements/tools.txt +# keep in sync with LATEST_SUPPORTED_PY in .github/workflows/ci-build.yml +LATEST_SUPPORTED_PY="3.14" +current_py=$(python --version | sed -E 's/Python ([0-9]+\.[0-9]+).*/\1/') + +./scripts/install.sh echo "Generating code ..." && python scripts/codegen.py --path . echo "Running black (code formatter) ..." && ./scripts/format.sh --no-install @@ -19,3 +21,8 @@ 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 + +# Run mypy type checking only on the latest supported Python version +if [[ "$current_py" == "$LATEST_SUPPORTED_PY" ]]; then + echo "Running mypy type checking ..." && ./scripts/run_mypy.sh --no-install +fi