diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 624f390..d6f0771 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -8,7 +8,6 @@ on: - 'tests/**' - 'scripts/**' - 'pyproject.toml' - - '.pyrefly-baseline.json' - '.github/workflows/lint.yml' pull_request: branches: [main] @@ -18,7 +17,6 @@ on: - 'tests/**' - 'scripts/**' - 'pyproject.toml' - - '.pyrefly-baseline.json' - '.github/workflows/lint.yml' workflow_dispatch: @@ -59,10 +57,10 @@ jobs: continue-on-error: true run: uv run python scripts/audit_test_quality.py - - name: Pyrefly type check (against baseline) + - name: Pyrefly type check id: pyrefly continue-on-error: true - run: uv run pyrefly check --baseline=.pyrefly-baseline.json + run: uv run pyrefly check - name: Minimize uv cache run: uv cache prune --ci @@ -80,9 +78,9 @@ jobs: echo "| Pyrefly | ${{ steps.pyrefly.outcome }} |" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY if [ "${{ steps.pyrefly.outcome }}" = "success" ]; then - echo "No new type issues above the baseline." >> $GITHUB_STEP_SUMMARY + echo "Zero type errors." >> $GITHUB_STEP_SUMMARY else - echo "New type issues above the baseline. Either fix them or refresh the baseline with \`uv run pyrefly check --baseline=.pyrefly-baseline.json --update-baseline\` if intentional." >> $GITHUB_STEP_SUMMARY + echo "Type errors detected — see run output." >> $GITHUB_STEP_SUMMARY fi - name: Fail if any step failed diff --git a/.pyrefly-baseline.json b/.pyrefly-baseline.json deleted file mode 100644 index de42515..0000000 --- a/.pyrefly-baseline.json +++ /dev/null @@ -1,2560 +0,0 @@ -{ - "errors": [ - { - "line": 276, - "column": 20, - "stop_line": 276, - "stop_column": 32, - "path": "src/chat_sdk/adapters/discord/adapter.py", - "code": -2, - "name": "missing-import", - "description": "Cannot find module `nacl.signing`\n Looked in these locations (from config in `/home/user/chat-sdk-python/pyproject.toml`):\n Import root (inferred from project layout): \"/home/user/chat-sdk-python/src\"\n Site package path queried from interpreter: [\"/home/user/chat-sdk-python/.venv/lib/python3.11/site-packages\", \"/home/user/chat-sdk-python/src\"]", - "concise_description": "Cannot find module `nacl.signing`", - "severity": "error" - }, - { - "line": 366, - "column": 25, - "stop_line": 366, - "stop_column": 29, - "path": "src/chat_sdk/adapters/discord/adapter.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `Self@DiscordAdapter` is not assignable to parameter `adapter` with type `Adapter` in function `chat_sdk.types.ActionEvent.__init__`\n Property getter for `Self@DiscordAdapter.lock_scope` has type `(self: Self@DiscordAdapter) -> str | None`, which is not assignable to `(self: Self@DiscordAdapter) -> Literal['channel', 'thread'] | None`, the property getter for `Adapter.lock_scope`", - "concise_description": "Argument `Self@DiscordAdapter` is not assignable to parameter `adapter` with type `Adapter` in function `chat_sdk.types.ActionEvent.__init__`", - "severity": "error" - }, - { - "line": 416, - "column": 74, - "stop_line": 416, - "stop_column": 83, - "path": "src/chat_sdk/adapters/discord/adapter.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `Literal['options']` is not assignable to parameter `key` with type `Literal['name']` in function `dict.get`", - "concise_description": "Argument `Literal['options']` is not assignable to parameter `key` with type `Literal['name']` in function `dict.get`", - "severity": "error" - }, - { - "line": 449, - "column": 21, - "stop_line": 449, - "stop_column": 25, - "path": "src/chat_sdk/adapters/discord/adapter.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `Self@DiscordAdapter` is not assignable to parameter `adapter` with type `Adapter` in function `chat_sdk.types.SlashCommandEvent.__init__`\n Property getter for `Self@DiscordAdapter.lock_scope` has type `(self: Self@DiscordAdapter) -> str | None`, which is not assignable to `(self: Self@DiscordAdapter) -> Literal['channel', 'thread'] | None`, the property getter for `Adapter.lock_scope`", - "concise_description": "Argument `Self@DiscordAdapter` is not assignable to parameter `adapter` with type `Adapter` in function `chat_sdk.types.SlashCommandEvent.__init__`", - "severity": "error" - }, - { - "line": 450, - "column": 21, - "stop_line": 450, - "stop_column": 25, - "path": "src/chat_sdk/adapters/discord/adapter.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `None` is not assignable to parameter `channel` with type `Channel` in function `chat_sdk.types.SlashCommandEvent.__init__`\n Protocol `Channel` requires attribute `name`", - "concise_description": "Argument `None` is not assignable to parameter `channel` with type `Channel` in function `chat_sdk.types.SlashCommandEvent.__init__`", - "severity": "error" - }, - { - "line": 599, - "column": 26, - "stop_line": 599, - "stop_column": 74, - "path": "src/chat_sdk/adapters/discord/adapter.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `str` is not assignable to parameter `type` with type `Literal['audio', 'file', 'image', 'video']` in function `chat_sdk.types.Attachment.__init__`", - "concise_description": "Argument `str` is not assignable to parameter `type` with type `Literal['audio', 'file', 'image', 'video']` in function `chat_sdk.types.Attachment.__init__`", - "severity": "error" - }, - { - "line": 612, - "column": 19, - "stop_line": 612, - "stop_column": 53, - "path": "src/chat_sdk/adapters/discord/adapter.py", - "code": -2, - "name": "missing-attribute", - "description": "Object of class `ChatInstance` has no attribute `handle_incoming_message`", - "concise_description": "Object of class `ChatInstance` has no attribute `handle_incoming_message`", - "severity": "error" - }, - { - "line": 706, - "column": 25, - "stop_line": 706, - "stop_column": 29, - "path": "src/chat_sdk/adapters/discord/adapter.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `Self@DiscordAdapter` is not assignable to parameter `adapter` with type `Adapter` in function `chat_sdk.types.ReactionEvent.__init__`\n Property getter for `Self@DiscordAdapter.lock_scope` has type `(self: Self@DiscordAdapter) -> str | None`, which is not assignable to `(self: Self@DiscordAdapter) -> Literal['channel', 'thread'] | None`, the property getter for `Adapter.lock_scope`", - "concise_description": "Argument `Self@DiscordAdapter` is not assignable to parameter `adapter` with type `Adapter` in function `chat_sdk.types.ReactionEvent.__init__`", - "severity": "error" - }, - { - "line": 707, - "column": 24, - "stop_line": 707, - "stop_column": 28, - "path": "src/chat_sdk/adapters/discord/adapter.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `None` is not assignable to parameter `thread` with type `Thread` in function `chat_sdk.types.ReactionEvent.__init__`\n Protocol `Thread` requires attribute `channel_id`", - "concise_description": "Argument `None` is not assignable to parameter `thread` with type `Thread` in function `chat_sdk.types.ReactionEvent.__init__`", - "severity": "error" - }, - { - "line": 1162, - "column": 48, - "stop_line": 1162, - "stop_column": 68, - "path": "src/chat_sdk/adapters/discord/adapter.py", - "code": -2, - "name": "bad-assignment", - "description": "`dict[str, str | Unknown]` is not assignable to `PostableAst | PostableCard | PostableMarkdown | PostableRaw | str | CardElement`", - "concise_description": "`dict[str, str | Unknown]` is not assignable to `PostableAst | PostableCard | PostableMarkdown | PostableRaw | str | CardElement`", - "severity": "error" - }, - { - "line": 1249, - "column": 26, - "stop_line": 1249, - "stop_column": 76, - "path": "src/chat_sdk/adapters/discord/adapter.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `str` is not assignable to parameter `type` with type `Literal['audio', 'file', 'image', 'video']` in function `chat_sdk.types.Attachment.__init__`", - "concise_description": "Argument `str` is not assignable to parameter `type` with type `Literal['audio', 'file', 'image', 'video']` in function `chat_sdk.types.Attachment.__init__`", - "severity": "error" - }, - { - "line": 1411, - "column": 24, - "stop_line": 1411, - "stop_column": 44, - "path": "src/chat_sdk/adapters/discord/adapter.py", - "code": -2, - "name": "not-async", - "description": "Type `object` is not awaitable", - "concise_description": "Type `object` is not awaitable", - "severity": "error" - }, - { - "line": 118, - "column": 46, - "stop_line": 118, - "stop_column": 51, - "path": "src/chat_sdk/adapters/discord/cards.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `object | str` is not assignable to parameter `text` with type `str` in function `_convert_emoji`", - "concise_description": "Argument `object | str` is not assignable to parameter `text` with type `str` in function `_convert_emoji`", - "severity": "error" - }, - { - "line": 122, - "column": 54, - "stop_line": 122, - "stop_column": 61, - "path": "src/chat_sdk/adapters/discord/cards.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `list[str] | object` is not assignable to parameter `headers` with type `list[str]` in function `chat_sdk.shared.card_utils.render_gfm_table`", - "concise_description": "Argument `list[str] | object` is not assignable to parameter `headers` with type `list[str]` in function `chat_sdk.shared.card_utils.render_gfm_table`", - "severity": "error" - }, - { - "line": 122, - "column": 63, - "stop_line": 122, - "stop_column": 67, - "path": "src/chat_sdk/adapters/discord/cards.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `list[list[str]] | object` is not assignable to parameter `rows` with type `list[list[str]]` in function `chat_sdk.shared.card_utils.render_gfm_table`", - "concise_description": "Argument `list[list[str]] | object` is not assignable to parameter `rows` with type `list[list[str]]` in function `chat_sdk.shared.card_utils.render_gfm_table`", - "severity": "error" - }, - { - "line": 275, - "column": 47, - "stop_line": 275, - "stop_column": 54, - "path": "src/chat_sdk/adapters/discord/cards.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `list[str] | object` is not assignable to parameter `headers` with type `list[str]` in function `chat_sdk.cards.table_element_to_ascii`", - "concise_description": "Argument `list[str] | object` is not assignable to parameter `headers` with type `list[str]` in function `chat_sdk.cards.table_element_to_ascii`", - "severity": "error" - }, - { - "line": 275, - "column": 56, - "stop_line": 275, - "stop_column": 60, - "path": "src/chat_sdk/adapters/discord/cards.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `list[list[str]] | object` is not assignable to parameter `rows` with type `list[list[str]]` in function `chat_sdk.cards.table_element_to_ascii`", - "concise_description": "Argument `list[list[str]] | object` is not assignable to parameter `rows` with type `list[list[str]]` in function `chat_sdk.cards.table_element_to_ascii`", - "severity": "error" - }, - { - "line": 121, - "column": 39, - "stop_line": 121, - "stop_column": 46, - "path": "src/chat_sdk/adapters/github/adapter.py", - "code": -2, - "name": "bad-typed-dict-key", - "description": "TypedDict `GitHubAdapterAppConfig` does not have key `token`", - "concise_description": "TypedDict `GitHubAdapterAppConfig` does not have key `token`", - "severity": "error" - }, - { - "line": 121, - "column": 39, - "stop_line": 121, - "stop_column": 46, - "path": "src/chat_sdk/adapters/github/adapter.py", - "code": -2, - "name": "bad-typed-dict-key", - "description": "TypedDict `GitHubAdapterAutoConfig` does not have key `token`", - "concise_description": "TypedDict `GitHubAdapterAutoConfig` does not have key `token`", - "severity": "error" - }, - { - "line": 121, - "column": 39, - "stop_line": 121, - "stop_column": 46, - "path": "src/chat_sdk/adapters/github/adapter.py", - "code": -2, - "name": "bad-typed-dict-key", - "description": "TypedDict `GitHubAdapterMultiTenantAppConfig` does not have key `token`", - "concise_description": "TypedDict `GitHubAdapterMultiTenantAppConfig` does not have key `token`", - "severity": "error" - }, - { - "line": 125, - "column": 38, - "stop_line": 125, - "stop_column": 46, - "path": "src/chat_sdk/adapters/github/adapter.py", - "code": -2, - "name": "bad-typed-dict-key", - "description": "TypedDict `GitHubAdapterAutoConfig` does not have key `app_id`", - "concise_description": "TypedDict `GitHubAdapterAutoConfig` does not have key `app_id`", - "severity": "error" - }, - { - "line": 125, - "column": 38, - "stop_line": 125, - "stop_column": 46, - "path": "src/chat_sdk/adapters/github/adapter.py", - "code": -2, - "name": "bad-typed-dict-key", - "description": "TypedDict `GitHubAdapterPATConfig` does not have key `app_id`", - "concise_description": "TypedDict `GitHubAdapterPATConfig` does not have key `app_id`", - "severity": "error" - }, - { - "line": 126, - "column": 43, - "stop_line": 126, - "stop_column": 56, - "path": "src/chat_sdk/adapters/github/adapter.py", - "code": -2, - "name": "bad-typed-dict-key", - "description": "TypedDict `GitHubAdapterAutoConfig` does not have key `private_key`", - "concise_description": "TypedDict `GitHubAdapterAutoConfig` does not have key `private_key`", - "severity": "error" - }, - { - "line": 126, - "column": 43, - "stop_line": 126, - "stop_column": 56, - "path": "src/chat_sdk/adapters/github/adapter.py", - "code": -2, - "name": "bad-typed-dict-key", - "description": "TypedDict `GitHubAdapterPATConfig` does not have key `private_key`", - "concise_description": "TypedDict `GitHubAdapterPATConfig` does not have key `private_key`", - "severity": "error" - }, - { - "line": 128, - "column": 48, - "stop_line": 128, - "stop_column": 65, - "path": "src/chat_sdk/adapters/github/adapter.py", - "code": -2, - "name": "bad-typed-dict-key", - "description": "TypedDict `GitHubAdapterAutoConfig` does not have key `installation_id`", - "concise_description": "TypedDict `GitHubAdapterAutoConfig` does not have key `installation_id`", - "severity": "error" - }, - { - "line": 128, - "column": 48, - "stop_line": 128, - "stop_column": 65, - "path": "src/chat_sdk/adapters/github/adapter.py", - "code": -2, - "name": "bad-typed-dict-key", - "description": "TypedDict `GitHubAdapterMultiTenantAppConfig` does not have key `installation_id`", - "concise_description": "TypedDict `GitHubAdapterMultiTenantAppConfig` does not have key `installation_id`", - "severity": "error" - }, - { - "line": 128, - "column": 48, - "stop_line": 128, - "stop_column": 65, - "path": "src/chat_sdk/adapters/github/adapter.py", - "code": -2, - "name": "bad-typed-dict-key", - "description": "TypedDict `GitHubAdapterPATConfig` does not have key `installation_id`", - "concise_description": "TypedDict `GitHubAdapterPATConfig` does not have key `installation_id`", - "severity": "error" - }, - { - "line": 131, - "column": 38, - "stop_line": 131, - "stop_column": 46, - "path": "src/chat_sdk/adapters/github/adapter.py", - "code": -2, - "name": "bad-typed-dict-key", - "description": "TypedDict `GitHubAdapterAutoConfig` does not have key `app_id`", - "concise_description": "TypedDict `GitHubAdapterAutoConfig` does not have key `app_id`", - "severity": "error" - }, - { - "line": 131, - "column": 38, - "stop_line": 131, - "stop_column": 46, - "path": "src/chat_sdk/adapters/github/adapter.py", - "code": -2, - "name": "bad-typed-dict-key", - "description": "TypedDict `GitHubAdapterPATConfig` does not have key `app_id`", - "concise_description": "TypedDict `GitHubAdapterPATConfig` does not have key `app_id`", - "severity": "error" - }, - { - "line": 132, - "column": 43, - "stop_line": 132, - "stop_column": 56, - "path": "src/chat_sdk/adapters/github/adapter.py", - "code": -2, - "name": "bad-typed-dict-key", - "description": "TypedDict `GitHubAdapterAutoConfig` does not have key `private_key`", - "concise_description": "TypedDict `GitHubAdapterAutoConfig` does not have key `private_key`", - "severity": "error" - }, - { - "line": 132, - "column": 43, - "stop_line": 132, - "stop_column": 56, - "path": "src/chat_sdk/adapters/github/adapter.py", - "code": -2, - "name": "bad-typed-dict-key", - "description": "TypedDict `GitHubAdapterPATConfig` does not have key `private_key`", - "concise_description": "TypedDict `GitHubAdapterPATConfig` does not have key `private_key`", - "severity": "error" - }, - { - "line": 317, - "column": 54, - "stop_line": 317, - "stop_column": 64, - "path": "src/chat_sdk/adapters/github/adapter.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `GitHubRepository` is not assignable to parameter `repository` with type `dict[str, Any]` in function `GitHubAdapter._parse_issue_comment`", - "concise_description": "Argument `GitHubRepository` is not assignable to parameter `repository` with type `dict[str, Any]` in function `GitHubAdapter._parse_issue_comment`", - "severity": "error" - }, - { - "line": 323, - "column": 36, - "stop_line": 323, - "stop_column": 40, - "path": "src/chat_sdk/adapters/github/adapter.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `Self@GitHubAdapter` is not assignable to parameter `adapter` with type `Adapter` in function `chat_sdk.types.ChatInstance.process_message`\n Property getter for `Self@GitHubAdapter.lock_scope` has type `(self: Self@GitHubAdapter) -> str | None`, which is not assignable to `(self: Self@GitHubAdapter) -> Literal['channel', 'thread'] | None`, the property getter for `Adapter.lock_scope`", - "concise_description": "Argument `Self@GitHubAdapter` is not assignable to parameter `adapter` with type `Adapter` in function `chat_sdk.types.ChatInstance.process_message`", - "severity": "error" - }, - { - "line": 352, - "column": 55, - "stop_line": 352, - "stop_column": 65, - "path": "src/chat_sdk/adapters/github/adapter.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `GitHubRepository` is not assignable to parameter `repository` with type `dict[str, Any]` in function `GitHubAdapter._parse_review_comment`", - "concise_description": "Argument `GitHubRepository` is not assignable to parameter `repository` with type `dict[str, Any]` in function `GitHubAdapter._parse_review_comment`", - "severity": "error" - }, - { - "line": 358, - "column": 36, - "stop_line": 358, - "stop_column": 40, - "path": "src/chat_sdk/adapters/github/adapter.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `Self@GitHubAdapter` is not assignable to parameter `adapter` with type `Adapter` in function `chat_sdk.types.ChatInstance.process_message`\n Property getter for `Self@GitHubAdapter.lock_scope` has type `(self: Self@GitHubAdapter) -> str | None`, which is not assignable to `(self: Self@GitHubAdapter) -> Literal['channel', 'thread'] | None`, the property getter for `Adapter.lock_scope`", - "concise_description": "Argument `Self@GitHubAdapter` is not assignable to parameter `adapter` with type `Adapter` in function `chat_sdk.types.ChatInstance.process_message`", - "severity": "error" - }, - { - "line": 526, - "column": 51, - "stop_line": 526, - "stop_column": 69, - "path": "src/chat_sdk/adapters/github/adapter.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `dict[str, str]` is not assignable to parameter `message` with type `PostableAst | PostableCard | PostableMarkdown | PostableRaw | str | CardElement` in function `GitHubAdapter.post_message`", - "concise_description": "Argument `dict[str, str]` is not assignable to parameter `message` with type `PostableAst | PostableCard | PostableMarkdown | PostableRaw | str | CardElement` in function `GitHubAdapter.post_message`", - "severity": "error" - }, - { - "line": 894, - "column": 26, - "stop_line": 894, - "stop_column": 37, - "path": "src/chat_sdk/adapters/github/adapter.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `object | str` is not assignable to parameter `type` with type `Literal['issue', 'pr'] | None` in function `chat_sdk.adapters.github.types.GitHubThreadId.__init__`", - "concise_description": "Argument `object | str` is not assignable to parameter `type` with type `Literal['issue', 'pr'] | None` in function `chat_sdk.adapters.github.types.GitHubThreadId.__init__`", - "severity": "error" - }, - { - "line": 898, - "column": 33, - "stop_line": 898, - "stop_column": 50, - "path": "src/chat_sdk/adapters/github/adapter.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `GitHubRepository` is not assignable to parameter `repository` with type `dict[str, Any]` in function `GitHubAdapter._parse_issue_comment`", - "concise_description": "Argument `GitHubRepository` is not assignable to parameter `repository` with type `dict[str, Any]` in function `GitHubAdapter._parse_issue_comment`", - "severity": "error" - }, - { - "line": 898, - "column": 81, - "stop_line": 898, - "stop_column": 92, - "path": "src/chat_sdk/adapters/github/adapter.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `object | str` is not assignable to parameter `thread_type` with type `Literal['issue', 'pr']` in function `GitHubAdapter._parse_issue_comment`", - "concise_description": "Argument `object | str` is not assignable to parameter `thread_type` with type `Literal['issue', 'pr']` in function `GitHubAdapter._parse_issue_comment`", - "severity": "error" - }, - { - "line": 907, - "column": 39, - "stop_line": 907, - "stop_column": 54, - "path": "src/chat_sdk/adapters/github/adapter.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `int | object` is not assignable to parameter `review_comment_id` with type `int | None` in function `chat_sdk.adapters.github.types.GitHubThreadId.__init__`", - "concise_description": "Argument `int | object` is not assignable to parameter `review_comment_id` with type `int | None` in function `chat_sdk.adapters.github.types.GitHubThreadId.__init__`", - "severity": "error" - }, - { - "line": 910, - "column": 47, - "stop_line": 910, - "stop_column": 61, - "path": "src/chat_sdk/adapters/github/adapter.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `GitHubIssueComment | GitHubReviewComment` is not assignable to parameter `comment` with type `GitHubReviewComment` in function `GitHubAdapter._parse_review_comment`\n Field `commit_id` is present in `GitHubReviewComment` and absent in `GitHubIssueComment`", - "concise_description": "Argument `GitHubIssueComment | GitHubReviewComment` is not assignable to parameter `comment` with type `GitHubReviewComment` in function `GitHubAdapter._parse_review_comment`", - "severity": "error" - }, - { - "line": 910, - "column": 63, - "stop_line": 910, - "stop_column": 80, - "path": "src/chat_sdk/adapters/github/adapter.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `GitHubRepository` is not assignable to parameter `repository` with type `dict[str, Any]` in function `GitHubAdapter._parse_review_comment`", - "concise_description": "Argument `GitHubRepository` is not assignable to parameter `repository` with type `dict[str, Any]` in function `GitHubAdapter._parse_review_comment`", - "severity": "error" - }, - { - "line": 1068, - "column": 20, - "stop_line": 1068, - "stop_column": 40, - "path": "src/chat_sdk/adapters/github/adapter.py", - "code": -2, - "name": "not-async", - "description": "Type `object` is not awaitable", - "concise_description": "Type `object` is not awaitable", - "severity": "error" - }, - { - "line": 99, - "column": 29, - "stop_line": 99, - "stop_column": 34, - "path": "src/chat_sdk/adapters/github/cards.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `ActionsElement | DividerElement | FieldsElement | ImageElement | LinkElement | SectionElement | TableElement | TextElement` is not assignable to parameter `text` with type `dict[str, Any]` in function `_render_text`", - "concise_description": "Argument `ActionsElement | DividerElement | FieldsElement | ImageElement | LinkElement | SectionElement | TableElement | TextElement` is not assignable to parameter `text` with type `dict[str, Any]` in function `_render_text`", - "severity": "error" - }, - { - "line": 102, - "column": 31, - "stop_line": 102, - "stop_column": 36, - "path": "src/chat_sdk/adapters/github/cards.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `ActionsElement | DividerElement | FieldsElement | ImageElement | LinkElement | SectionElement | TableElement | TextElement` is not assignable to parameter `fields` with type `dict[str, Any]` in function `_render_fields`", - "concise_description": "Argument `ActionsElement | DividerElement | FieldsElement | ImageElement | LinkElement | SectionElement | TableElement | TextElement` is not assignable to parameter `fields` with type `dict[str, Any]` in function `_render_fields`", - "severity": "error" - }, - { - "line": 105, - "column": 32, - "stop_line": 105, - "stop_column": 37, - "path": "src/chat_sdk/adapters/github/cards.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `ActionsElement | DividerElement | FieldsElement | ImageElement | LinkElement | SectionElement | TableElement | TextElement` is not assignable to parameter `actions` with type `dict[str, Any]` in function `_render_actions`", - "concise_description": "Argument `ActionsElement | DividerElement | FieldsElement | ImageElement | LinkElement | SectionElement | TableElement | TextElement` is not assignable to parameter `actions` with type `dict[str, Any]` in function `_render_actions`", - "severity": "error" - }, - { - "line": 110, - "column": 30, - "stop_line": 110, - "stop_column": 55, - "path": "src/chat_sdk/adapters/github/cards.py", - "code": -2, - "name": "not-iterable", - "description": "Type `object` is not iterable", - "concise_description": "Type `object` is not iterable", - "severity": "error" - }, - { - "line": 118, - "column": 43, - "stop_line": 118, - "stop_column": 46, - "path": "src/chat_sdk/adapters/github/cards.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `object | str` is not assignable to parameter `text` with type `str` in function `_escape_markdown`", - "concise_description": "Argument `object | str` is not assignable to parameter `text` with type `str` in function `_escape_markdown`", - "severity": "error" - }, - { - "line": 124, - "column": 38, - "stop_line": 124, - "stop_column": 43, - "path": "src/chat_sdk/adapters/github/cards.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `object | str` is not assignable to parameter `text` with type `str` in function `_escape_markdown`", - "concise_description": "Argument `object | str` is not assignable to parameter `text` with type `str` in function `_escape_markdown`", - "severity": "error" - }, - { - "line": 130, - "column": 30, - "stop_line": 130, - "stop_column": 35, - "path": "src/chat_sdk/adapters/github/cards.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `ActionsElement | DividerElement | FieldsElement | ImageElement | LinkElement | SectionElement | TableElement | TextElement` is not assignable to parameter `table` with type `dict[str, Any]` in function `_render_table`", - "concise_description": "Argument `ActionsElement | DividerElement | FieldsElement | ImageElement | LinkElement | SectionElement | TableElement | TextElement` is not assignable to parameter `table` with type `dict[str, Any]` in function `_render_table`", - "severity": "error" - }, - { - "line": 236, - "column": 40, - "stop_line": 236, - "stop_column": 45, - "path": "src/chat_sdk/adapters/github/cards.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `ActionsElement | DividerElement | FieldsElement | ImageElement | LinkElement | SectionElement | TableElement | TextElement` is not assignable to parameter `table` with type `dict[str, Any]` in function `_render_table`", - "concise_description": "Argument `ActionsElement | DividerElement | FieldsElement | ImageElement | LinkElement | SectionElement | TableElement | TextElement` is not assignable to parameter `table` with type `dict[str, Any]` in function `_render_table`", - "severity": "error" - }, - { - "line": 37, - "column": 9, - "stop_line": 37, - "stop_column": 15, - "path": "src/chat_sdk/adapters/github/format_converter.py", - "code": -2, - "name": "bad-param-name-override", - "description": "Class member `GitHubFormatConverter.to_ast` overrides parent class `BaseFormatConverter` in an inconsistent manner\n Got parameter name `markdown`, expected `platform_text`", - "concise_description": "Class member `GitHubFormatConverter.to_ast` overrides parent class `BaseFormatConverter` in an inconsistent manner", - "severity": "error" - }, - { - "line": 326, - "column": 20, - "stop_line": 326, - "stop_column": 31, - "path": "src/chat_sdk/adapters/google_chat/adapter.py", - "code": -2, - "name": "missing-import", - "description": "Cannot find module `google.auth`\n Looked in these locations (from config in `/home/user/chat-sdk-python/pyproject.toml`):\n Import root (inferred from project layout): \"/home/user/chat-sdk-python/src\"\n Site package path queried from interpreter: [\"/home/user/chat-sdk-python/.venv/lib/python3.11/site-packages\", \"/home/user/chat-sdk-python/src\"]", - "concise_description": "Cannot find module `google.auth`", - "severity": "error" - }, - { - "line": 327, - "column": 20, - "stop_line": 327, - "stop_column": 50, - "path": "src/chat_sdk/adapters/google_chat/adapter.py", - "code": -2, - "name": "missing-import", - "description": "Cannot find module `google.auth.transport.requests`\n Looked in these locations (from config in `/home/user/chat-sdk-python/pyproject.toml`):\n Import root (inferred from project layout): \"/home/user/chat-sdk-python/src\"\n Site package path queried from interpreter: [\"/home/user/chat-sdk-python/.venv/lib/python3.11/site-packages\", \"/home/user/chat-sdk-python/src\"]", - "concise_description": "Cannot find module `google.auth.transport.requests`", - "severity": "error" - }, - { - "line": 764, - "column": 20, - "stop_line": 764, - "stop_column": 40, - "path": "src/chat_sdk/adapters/google_chat/adapter.py", - "code": -2, - "name": "not-async", - "description": "Type `object` is not awaitable", - "concise_description": "Type `object` is not awaitable", - "severity": "error" - }, - { - "line": 954, - "column": 13, - "stop_line": 954, - "stop_column": 17, - "path": "src/chat_sdk/adapters/google_chat/adapter.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `Self@GoogleChatAdapter` is not assignable to parameter `adapter` with type `Adapter` in function `chat_sdk.types.ChatInstance.process_message`\n Property getter for `Self@GoogleChatAdapter.lock_scope` has type `(self: Self@GoogleChatAdapter) -> str | None`, which is not assignable to `(self: Self@GoogleChatAdapter) -> Literal['channel', 'thread'] | None`, the property getter for `Adapter.lock_scope`", - "concise_description": "Argument `Self@GoogleChatAdapter` is not assignable to parameter `adapter` with type `Adapter` in function `chat_sdk.types.ChatInstance.process_message`", - "severity": "error" - }, - { - "line": 1024, - "column": 25, - "stop_line": 1024, - "stop_column": 29, - "path": "src/chat_sdk/adapters/google_chat/adapter.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `Self@GoogleChatAdapter` is not assignable to parameter `adapter` with type `Adapter` in function `chat_sdk.types.ReactionEvent.__init__`\n Property getter for `Self@GoogleChatAdapter.lock_scope` has type `(self: Self@GoogleChatAdapter) -> str | None`, which is not assignable to `(self: Self@GoogleChatAdapter) -> Literal['channel', 'thread'] | None`, the property getter for `Adapter.lock_scope`", - "concise_description": "Argument `Self@GoogleChatAdapter` is not assignable to parameter `adapter` with type `Adapter` in function `chat_sdk.types.ReactionEvent.__init__`", - "severity": "error" - }, - { - "line": 1025, - "column": 24, - "stop_line": 1025, - "stop_column": 28, - "path": "src/chat_sdk/adapters/google_chat/adapter.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `None` is not assignable to parameter `thread` with type `Thread` in function `chat_sdk.types.ReactionEvent.__init__`\n Protocol `Thread` requires attribute `channel_id`", - "concise_description": "Argument `None` is not assignable to parameter `thread` with type `Thread` in function `chat_sdk.types.ReactionEvent.__init__`", - "severity": "error" - }, - { - "line": 1192, - "column": 21, - "stop_line": 1192, - "stop_column": 25, - "path": "src/chat_sdk/adapters/google_chat/adapter.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `Self@GoogleChatAdapter` is not assignable to parameter `adapter` with type `Adapter` in function `chat_sdk.types.ActionEvent.__init__`\n Property getter for `Self@GoogleChatAdapter.lock_scope` has type `(self: Self@GoogleChatAdapter) -> str | None`, which is not assignable to `(self: Self@GoogleChatAdapter) -> Literal['channel', 'thread'] | None`, the property getter for `Adapter.lock_scope`", - "concise_description": "Argument `Self@GoogleChatAdapter` is not assignable to parameter `adapter` with type `Adapter` in function `chat_sdk.types.ActionEvent.__init__`", - "severity": "error" - }, - { - "line": 1252, - "column": 13, - "stop_line": 1252, - "stop_column": 17, - "path": "src/chat_sdk/adapters/google_chat/adapter.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `Self@GoogleChatAdapter` is not assignable to parameter `adapter` with type `Adapter` in function `chat_sdk.types.ChatInstance.process_message`\n Property getter for `Self@GoogleChatAdapter.lock_scope` has type `(self: Self@GoogleChatAdapter) -> str | None`, which is not assignable to `(self: Self@GoogleChatAdapter) -> Literal['channel', 'thread'] | None`, the property getter for `Adapter.lock_scope`", - "concise_description": "Argument `Self@GoogleChatAdapter` is not assignable to parameter `adapter` with type `Adapter` in function `chat_sdk.types.ChatInstance.process_message`", - "severity": "error" - }, - { - "line": 1320, - "column": 10, - "stop_line": 1320, - "stop_column": 20, - "path": "src/chat_sdk/adapters/google_chat/adapter.py", - "code": -2, - "name": "bad-return", - "description": "Function declared to return `RawMessage`, but one or more paths are missing an explicit `return`", - "concise_description": "Function declared to return `RawMessage`, but one or more paths are missing an explicit `return`", - "severity": "error" - }, - { - "line": 1430, - "column": 10, - "stop_line": 1430, - "stop_column": 26, - "path": "src/chat_sdk/adapters/google_chat/adapter.py", - "code": -2, - "name": "bad-return", - "description": "Function declared to return `EphemeralMessage`, but one or more paths are missing an explicit `return`", - "concise_description": "Function declared to return `EphemeralMessage`, but one or more paths are missing an explicit `return`", - "severity": "error" - }, - { - "line": 1507, - "column": 10, - "stop_line": 1507, - "stop_column": 20, - "path": "src/chat_sdk/adapters/google_chat/adapter.py", - "code": -2, - "name": "bad-return", - "description": "Function declared to return `RawMessage`, but one or more paths are missing an explicit `return`", - "concise_description": "Function declared to return `RawMessage`, but one or more paths are missing an explicit `return`", - "severity": "error" - }, - { - "line": 1597, - "column": 51, - "stop_line": 1597, - "stop_column": 76, - "path": "src/chat_sdk/adapters/google_chat/adapter.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `dict[str, str]` is not assignable to parameter `message` with type `PostableAst | PostableCard | PostableMarkdown | PostableRaw | str | CardElement` in function `GoogleChatAdapter.post_message`", - "concise_description": "Argument `dict[str, str]` is not assignable to parameter `message` with type `PostableAst | PostableCard | PostableMarkdown | PostableRaw | str | CardElement` in function `GoogleChatAdapter.post_message`", - "severity": "error" - }, - { - "line": 1690, - "column": 46, - "stop_line": 1690, - "stop_column": 49, - "path": "src/chat_sdk/adapters/google_chat/adapter.py", - "code": -2, - "name": "bad-return", - "description": "Function declared to return `str`, but one or more paths are missing an explicit `return`", - "concise_description": "Function declared to return `str`, but one or more paths are missing an explicit `return`", - "severity": "error" - }, - { - "line": 1788, - "column": 10, - "stop_line": 1788, - "stop_column": 21, - "path": "src/chat_sdk/adapters/google_chat/adapter.py", - "code": -2, - "name": "bad-return", - "description": "Function declared to return `FetchResult`, but one or more paths are missing an explicit `return`", - "concise_description": "Function declared to return `FetchResult`, but one or more paths are missing an explicit `return`", - "severity": "error" - }, - { - "line": 1999, - "column": 53, - "stop_line": 1999, - "stop_column": 63, - "path": "src/chat_sdk/adapters/google_chat/adapter.py", - "code": -2, - "name": "bad-return", - "description": "Function declared to return `ThreadInfo`, but one or more paths are missing an explicit `return`", - "concise_description": "Function declared to return `ThreadInfo`, but one or more paths are missing an explicit `return`", - "severity": "error" - }, - { - "line": 2039, - "column": 10, - "stop_line": 2039, - "stop_column": 21, - "path": "src/chat_sdk/adapters/google_chat/adapter.py", - "code": -2, - "name": "bad-return", - "description": "Function declared to return `FetchResult`, but one or more paths are missing an explicit `return`", - "concise_description": "Function declared to return `FetchResult`, but one or more paths are missing an explicit `return`", - "severity": "error" - }, - { - "line": 2232, - "column": 10, - "stop_line": 2232, - "stop_column": 27, - "path": "src/chat_sdk/adapters/google_chat/adapter.py", - "code": -2, - "name": "bad-return", - "description": "Function declared to return `ListThreadsResult`, but one or more paths are missing an explicit `return`", - "concise_description": "Function declared to return `ListThreadsResult`, but one or more paths are missing an explicit `return`", - "severity": "error" - }, - { - "line": 2336, - "column": 60, - "stop_line": 2336, - "stop_column": 71, - "path": "src/chat_sdk/adapters/google_chat/adapter.py", - "code": -2, - "name": "bad-return", - "description": "Function declared to return `ChannelInfo`, but one or more paths are missing an explicit `return`", - "concise_description": "Function declared to return `ChannelInfo`, but one or more paths are missing an explicit `return`", - "severity": "error" - }, - { - "line": 2384, - "column": 10, - "stop_line": 2384, - "stop_column": 20, - "path": "src/chat_sdk/adapters/google_chat/adapter.py", - "code": -2, - "name": "bad-return", - "description": "Function declared to return `RawMessage`, but one or more paths are missing an explicit `return`", - "concise_description": "Function declared to return `RawMessage`, but one or more paths are missing an explicit `return`", - "severity": "error" - }, - { - "line": 76, - "column": 59, - "stop_line": 76, - "stop_column": 64, - "path": "src/chat_sdk/adapters/google_chat/cards.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `ActionsElement | DividerElement | FieldsElement | ImageElement | LinkElement | SectionElement | TableElement | TextElement | Unknown` is not assignable to parameter `element` with type `dict[str, Any]` in function `_convert_section_to_widgets`", - "concise_description": "Argument `ActionsElement | DividerElement | FieldsElement | ImageElement | LinkElement | SectionElement | TableElement | TextElement | Unknown` is not assignable to parameter `element` with type `dict[str, Any]` in function `_convert_section_to_widgets`", - "severity": "error" - }, - { - "line": 115, - "column": 41, - "stop_line": 115, - "stop_column": 46, - "path": "src/chat_sdk/adapters/google_chat/cards.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `ActionsElement | DividerElement | FieldsElement | ImageElement | LinkElement | SectionElement | TableElement | TextElement` is not assignable to parameter `element` with type `dict[str, Any]` in function `_convert_text_to_widget`", - "concise_description": "Argument `ActionsElement | DividerElement | FieldsElement | ImageElement | LinkElement | SectionElement | TableElement | TextElement` is not assignable to parameter `element` with type `dict[str, Any]` in function `_convert_text_to_widget`", - "severity": "error" - }, - { - "line": 117, - "column": 42, - "stop_line": 117, - "stop_column": 47, - "path": "src/chat_sdk/adapters/google_chat/cards.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `ActionsElement | DividerElement | FieldsElement | ImageElement | LinkElement | SectionElement | TableElement | TextElement` is not assignable to parameter `element` with type `dict[str, Any]` in function `_convert_image_to_widget`", - "concise_description": "Argument `ActionsElement | DividerElement | FieldsElement | ImageElement | LinkElement | SectionElement | TableElement | TextElement` is not assignable to parameter `element` with type `dict[str, Any]` in function `_convert_image_to_widget`", - "severity": "error" - }, - { - "line": 121, - "column": 44, - "stop_line": 121, - "stop_column": 49, - "path": "src/chat_sdk/adapters/google_chat/cards.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `ActionsElement | DividerElement | FieldsElement | ImageElement | LinkElement | SectionElement | TableElement | TextElement` is not assignable to parameter `element` with type `dict[str, Any]` in function `_convert_actions_to_widget`", - "concise_description": "Argument `ActionsElement | DividerElement | FieldsElement | ImageElement | LinkElement | SectionElement | TableElement | TextElement` is not assignable to parameter `element` with type `dict[str, Any]` in function `_convert_actions_to_widget`", - "severity": "error" - }, - { - "line": 123, - "column": 44, - "stop_line": 123, - "stop_column": 49, - "path": "src/chat_sdk/adapters/google_chat/cards.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `ActionsElement | DividerElement | FieldsElement | ImageElement | LinkElement | SectionElement | TableElement | TextElement` is not assignable to parameter `element` with type `dict[str, Any]` in function `_convert_section_to_widgets`", - "concise_description": "Argument `ActionsElement | DividerElement | FieldsElement | ImageElement | LinkElement | SectionElement | TableElement | TextElement` is not assignable to parameter `element` with type `dict[str, Any]` in function `_convert_section_to_widgets`", - "severity": "error" - }, - { - "line": 125, - "column": 43, - "stop_line": 125, - "stop_column": 48, - "path": "src/chat_sdk/adapters/google_chat/cards.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `ActionsElement | DividerElement | FieldsElement | ImageElement | LinkElement | SectionElement | TableElement | TextElement` is not assignable to parameter `element` with type `dict[str, Any]` in function `_convert_fields_to_widgets`", - "concise_description": "Argument `ActionsElement | DividerElement | FieldsElement | ImageElement | LinkElement | SectionElement | TableElement | TextElement` is not assignable to parameter `element` with type `dict[str, Any]` in function `_convert_fields_to_widgets`", - "severity": "error" - }, - { - "line": 132, - "column": 62, - "stop_line": 132, - "stop_column": 67, - "path": "src/chat_sdk/adapters/google_chat/cards.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `object | str` is not assignable to parameter with type `str`", - "concise_description": "Argument `object | str` is not assignable to parameter with type `str`", - "severity": "error" - }, - { - "line": 137, - "column": 42, - "stop_line": 137, - "stop_column": 47, - "path": "src/chat_sdk/adapters/google_chat/cards.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `ActionsElement | DividerElement | FieldsElement | ImageElement | LinkElement | SectionElement | TableElement | TextElement` is not assignable to parameter `element` with type `dict[str, Any]` in function `_convert_table_to_widget`", - "concise_description": "Argument `ActionsElement | DividerElement | FieldsElement | ImageElement | LinkElement | SectionElement | TableElement | TextElement` is not assignable to parameter `element` with type `dict[str, Any]` in function `_convert_table_to_widget`", - "severity": "error" - }, - { - "line": 37, - "column": 9, - "stop_line": 37, - "stop_column": 15, - "path": "src/chat_sdk/adapters/google_chat/format_converter.py", - "code": -2, - "name": "bad-param-name-override", - "description": "Class member `GoogleChatFormatConverter.to_ast` overrides parent class `BaseFormatConverter` in an inconsistent manner\n Got parameter name `gchat_text`, expected `platform_text`", - "concise_description": "Class member `GoogleChatFormatConverter.to_ast` overrides parent class `BaseFormatConverter` in an inconsistent manner", - "severity": "error" - }, - { - "line": 54, - "column": 9, - "stop_line": 54, - "stop_column": 27, - "path": "src/chat_sdk/adapters/google_chat/format_converter.py", - "code": -2, - "name": "bad-param-name-override", - "description": "Class member `GoogleChatFormatConverter.extract_plain_text` overrides parent class `BaseFormatConverter` in an inconsistent manner\n Got parameter name `text`, expected `platform_text`", - "concise_description": "Class member `GoogleChatFormatConverter.extract_plain_text` overrides parent class `BaseFormatConverter` in an inconsistent manner", - "severity": "error" - }, - { - "line": 346, - "column": 20, - "stop_line": 346, - "stop_column": 39, - "path": "src/chat_sdk/adapters/google_chat/workspace_events.py", - "code": -2, - "name": "not-async", - "description": "Type `object` is not awaitable", - "concise_description": "Type `object` is not awaitable", - "severity": "error" - }, - { - "line": 406, - "column": 16, - "stop_line": 406, - "stop_column": 27, - "path": "src/chat_sdk/adapters/google_chat/workspace_events.py", - "code": -2, - "name": "missing-import", - "description": "Cannot find module `google.auth`\n Looked in these locations (from config in `/home/user/chat-sdk-python/pyproject.toml`):\n Import root (inferred from project layout): \"/home/user/chat-sdk-python/src\"\n Site package path queried from interpreter: [\"/home/user/chat-sdk-python/.venv/lib/python3.11/site-packages\", \"/home/user/chat-sdk-python/src\"]", - "concise_description": "Cannot find module `google.auth`", - "severity": "error" - }, - { - "line": 407, - "column": 16, - "stop_line": 407, - "stop_column": 46, - "path": "src/chat_sdk/adapters/google_chat/workspace_events.py", - "code": -2, - "name": "missing-import", - "description": "Cannot find module `google.auth.transport.requests`\n Looked in these locations (from config in `/home/user/chat-sdk-python/pyproject.toml`):\n Import root (inferred from project layout): \"/home/user/chat-sdk-python/src\"\n Site package path queried from interpreter: [\"/home/user/chat-sdk-python/.venv/lib/python3.11/site-packages\", \"/home/user/chat-sdk-python/src\"]", - "concise_description": "Cannot find module `google.auth.transport.requests`", - "severity": "error" - }, - { - "line": 305, - "column": 46, - "stop_line": 305, - "stop_column": 53, - "path": "src/chat_sdk/adapters/linear/adapter.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `dict[str, Any]` is not assignable to parameter `payload` with type `CommentWebhookPayload` in function `LinearAdapter._handle_comment_created`", - "concise_description": "Argument `dict[str, Any]` is not assignable to parameter `payload` with type `CommentWebhookPayload` in function `LinearAdapter._handle_comment_created`", - "severity": "error" - }, - { - "line": 307, - "column": 35, - "stop_line": 307, - "stop_column": 42, - "path": "src/chat_sdk/adapters/linear/adapter.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `dict[str, Any]` is not assignable to parameter `payload` with type `ReactionWebhookPayload` in function `LinearAdapter._handle_reaction`", - "concise_description": "Argument `dict[str, Any]` is not assignable to parameter `payload` with type `ReactionWebhookPayload` in function `LinearAdapter._handle_reaction`", - "severity": "error" - }, - { - "line": 354, - "column": 26, - "stop_line": 354, - "stop_column": 34, - "path": "src/chat_sdk/adapters/linear/adapter.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `object | str | Unknown` is not assignable to parameter `issue_id` with type `str` in function `chat_sdk.adapters.linear.types.LinearThreadId.__init__`", - "concise_description": "Argument `object | str | Unknown` is not assignable to parameter `issue_id` with type `str` in function `chat_sdk.adapters.linear.types.LinearThreadId.__init__`", - "severity": "error" - }, - { - "line": 355, - "column": 28, - "stop_line": 355, - "stop_column": 43, - "path": "src/chat_sdk/adapters/linear/adapter.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `object | str | Unknown | None` is not assignable to parameter `comment_id` with type `str | None` in function `chat_sdk.adapters.linear.types.LinearThreadId.__init__`", - "concise_description": "Argument `object | str | Unknown | None` is not assignable to parameter `comment_id` with type `str | None` in function `chat_sdk.adapters.linear.types.LinearThreadId.__init__`", - "severity": "error" - }, - { - "line": 359, - "column": 39, - "stop_line": 359, - "stop_column": 43, - "path": "src/chat_sdk/adapters/linear/adapter.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `dict[Unknown, Unknown] | LinearCommentData` is not assignable to parameter `comment` with type `LinearCommentData` in function `LinearAdapter._build_message`", - "concise_description": "Argument `dict[Unknown, Unknown] | LinearCommentData` is not assignable to parameter `comment` with type `LinearCommentData` in function `LinearAdapter._build_message`", - "severity": "error" - }, - { - "line": 359, - "column": 45, - "stop_line": 359, - "stop_column": 50, - "path": "src/chat_sdk/adapters/linear/adapter.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `dict[Unknown, Unknown] | LinearWebhookActor` is not assignable to parameter `actor` with type `LinearWebhookActor` in function `LinearAdapter._build_message`", - "concise_description": "Argument `dict[Unknown, Unknown] | LinearWebhookActor` is not assignable to parameter `actor` with type `LinearWebhookActor` in function `LinearAdapter._build_message`", - "severity": "error" - }, - { - "line": 367, - "column": 36, - "stop_line": 367, - "stop_column": 40, - "path": "src/chat_sdk/adapters/linear/adapter.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `Self@LinearAdapter` is not assignable to parameter `adapter` with type `Adapter` in function `chat_sdk.types.ChatInstance.process_message`\n Property getter for `Self@LinearAdapter.lock_scope` has type `(self: Self@LinearAdapter) -> str | None`, which is not assignable to `(self: Self@LinearAdapter) -> Literal['channel', 'thread'] | None`, the property getter for `Adapter.lock_scope`", - "concise_description": "Argument `Self@LinearAdapter` is not assignable to parameter `adapter` with type `Adapter` in function `chat_sdk.types.ChatInstance.process_message`", - "severity": "error" - }, - { - "line": 399, - "column": 21, - "stop_line": 399, - "stop_column": 28, - "path": "src/chat_sdk/adapters/linear/adapter.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `object | str` is not assignable to parameter `user_id` with type `str` in function `chat_sdk.types.Author.__init__`", - "concise_description": "Argument `object | str` is not assignable to parameter `user_id` with type `str` in function `chat_sdk.types.Author.__init__`", - "severity": "error" - }, - { - "line": 419, - "column": 38, - "stop_line": 419, - "stop_column": 48, - "path": "src/chat_sdk/adapters/linear/adapter.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `object | str` is not assignable to parameter `s` with type `str` in function `chat_sdk.types._parse_iso`", - "concise_description": "Argument `object | str` is not assignable to parameter `s` with type `str` in function `chat_sdk.types._parse_iso`", - "severity": "error" - }, - { - "line": 421, - "column": 38, - "stop_line": 421, - "stop_column": 48, - "path": "src/chat_sdk/adapters/linear/adapter.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `object | str` is not assignable to parameter `s` with type `str` in function `chat_sdk.types._parse_iso`", - "concise_description": "Argument `object | str` is not assignable to parameter `s` with type `str` in function `chat_sdk.types._parse_iso`", - "severity": "error" - }, - { - "line": 742, - "column": 28, - "stop_line": 742, - "stop_column": 43, - "path": "src/chat_sdk/adapters/linear/adapter.py", - "code": -2, - "name": "bad-typed-dict-key", - "description": "`Any | None` is not assignable to TypedDict key `url` with type `str`", - "concise_description": "`Any | None` is not assignable to TypedDict key `url` with type `str`", - "severity": "error" - }, - { - "line": 849, - "column": 25, - "stop_line": 849, - "stop_column": 32, - "path": "src/chat_sdk/adapters/linear/adapter.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `object | str | Unknown` is not assignable to parameter `user_id` with type `str` in function `chat_sdk.types.Author.__init__`", - "concise_description": "Argument `object | str | Unknown` is not assignable to parameter `user_id` with type `str` in function `chat_sdk.types.Author.__init__`", - "severity": "error" - }, - { - "line": 856, - "column": 39, - "stop_line": 856, - "stop_column": 49, - "path": "src/chat_sdk/adapters/linear/adapter.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `object | str | Unknown` is not assignable to parameter `s` with type `str` in function `chat_sdk.types._parse_iso`", - "concise_description": "Argument `object | str | Unknown` is not assignable to parameter `s` with type `str` in function `chat_sdk.types._parse_iso`", - "severity": "error" - }, - { - "line": 858, - "column": 39, - "stop_line": 858, - "stop_column": 49, - "path": "src/chat_sdk/adapters/linear/adapter.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `object | str | Unknown` is not assignable to parameter `s` with type `str` in function `chat_sdk.types._parse_iso`", - "concise_description": "Argument `object | str | Unknown` is not assignable to parameter `s` with type `str` in function `chat_sdk.types._parse_iso`", - "severity": "error" - }, - { - "line": 894, - "column": 48, - "stop_line": 894, - "stop_column": 68, - "path": "src/chat_sdk/adapters/linear/adapter.py", - "code": -2, - "name": "bad-assignment", - "description": "`dict[str, str | Unknown]` is not assignable to `PostableAst | PostableCard | PostableMarkdown | PostableRaw | str | CardElement`", - "concise_description": "`dict[str, str | Unknown]` is not assignable to `PostableAst | PostableCard | PostableMarkdown | PostableRaw | str | CardElement`", - "severity": "error" - }, - { - "line": 973, - "column": 24, - "stop_line": 973, - "stop_column": 44, - "path": "src/chat_sdk/adapters/linear/adapter.py", - "code": -2, - "name": "not-async", - "description": "Type `object` is not awaitable", - "concise_description": "Type `object` is not awaitable", - "severity": "error" - }, - { - "line": 94, - "column": 43, - "stop_line": 94, - "stop_column": 46, - "path": "src/chat_sdk/adapters/linear/cards.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `object | str` is not assignable to parameter `text` with type `str` in function `_escape_markdown`", - "concise_description": "Argument `object | str` is not assignable to parameter `text` with type `str` in function `_escape_markdown`", - "severity": "error" - }, - { - "line": 99, - "column": 38, - "stop_line": 99, - "stop_column": 43, - "path": "src/chat_sdk/adapters/linear/cards.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `object | str` is not assignable to parameter `text` with type `str` in function `_escape_markdown`", - "concise_description": "Argument `object | str` is not assignable to parameter `text` with type `str` in function `_escape_markdown`", - "severity": "error" - }, - { - "line": 190, - "column": 43, - "stop_line": 190, - "stop_column": 50, - "path": "src/chat_sdk/adapters/linear/cards.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `list[str] | object` is not assignable to parameter `headers` with type `list[str]` in function `chat_sdk.shared.card_utils.render_gfm_table`", - "concise_description": "Argument `list[str] | object` is not assignable to parameter `headers` with type `list[str]` in function `chat_sdk.shared.card_utils.render_gfm_table`", - "severity": "error" - }, - { - "line": 190, - "column": 52, - "stop_line": 190, - "stop_column": 56, - "path": "src/chat_sdk/adapters/linear/cards.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `list[list[str]] | object` is not assignable to parameter `rows` with type `list[list[str]]` in function `chat_sdk.shared.card_utils.render_gfm_table`", - "concise_description": "Argument `list[list[str]] | object` is not assignable to parameter `rows` with type `list[list[str]]` in function `chat_sdk.shared.card_utils.render_gfm_table`", - "severity": "error" - }, - { - "line": 290, - "column": 9, - "stop_line": 290, - "stop_column": 62, - "path": "src/chat_sdk/adapters/slack/adapter.py", - "code": -2, - "name": "missing-import", - "description": "Cannot find module `slack_sdk.web.async_client`\n Looked in these locations (from config in `/home/user/chat-sdk-python/pyproject.toml`):\n Import root (inferred from project layout): \"/home/user/chat-sdk-python/src\"\n Site package path queried from interpreter: [\"/home/user/chat-sdk-python/.venv/lib/python3.11/site-packages\", \"/home/user/chat-sdk-python/src\"]", - "concise_description": "Cannot find module `slack_sdk.web.async_client`", - "severity": "error" - }, - { - "line": 401, - "column": 24, - "stop_line": 401, - "stop_column": 43, - "path": "src/chat_sdk/adapters/slack/adapter.py", - "code": -2, - "name": "unsupported-operation", - "description": "`None` is not subscriptable", - "concise_description": "`None` is not subscriptable", - "severity": "error" - }, - { - "line": 402, - "column": 26, - "stop_line": 402, - "stop_column": 47, - "path": "src/chat_sdk/adapters/slack/adapter.py", - "code": -2, - "name": "unsupported-operation", - "description": "`None` is not subscriptable", - "concise_description": "`None` is not subscriptable", - "severity": "error" - }, - { - "line": 403, - "column": 25, - "stop_line": 403, - "stop_column": 45, - "path": "src/chat_sdk/adapters/slack/adapter.py", - "code": -2, - "name": "unsupported-operation", - "description": "`None` is not subscriptable", - "concise_description": "`None` is not subscriptable", - "severity": "error" - }, - { - "line": 646, - "column": 25, - "stop_line": 646, - "stop_column": 45, - "path": "src/chat_sdk/adapters/slack/adapter.py", - "code": -2, - "name": "not-async", - "description": "Type `object` is not awaitable", - "concise_description": "Type `object` is not awaitable", - "severity": "error" - }, - { - "line": 667, - "column": 12, - "stop_line": 667, - "stop_column": 63, - "path": "src/chat_sdk/adapters/slack/adapter.py", - "code": -2, - "name": "not-iterable", - "description": "`in` is not supported between `Literal['application/x-www-form-urlencoded']` and `None`", - "concise_description": "`in` is not supported between `Literal['application/x-www-form-urlencoded']` and `None`", - "severity": "error" - }, - { - "line": 865, - "column": 21, - "stop_line": 865, - "stop_column": 25, - "path": "src/chat_sdk/adapters/slack/adapter.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `Self@SlackAdapter` is not assignable to parameter `adapter` with type `Adapter` in function `chat_sdk.types.SlashCommandEvent.__init__`\n Property getter for `Self@SlackAdapter.lock_scope` has type `(self: Self@SlackAdapter) -> str`, which is not assignable to `(self: Self@SlackAdapter) -> Literal['channel', 'thread'] | None`, the property getter for `Adapter.lock_scope`", - "concise_description": "Argument `Self@SlackAdapter` is not assignable to parameter `adapter` with type `Adapter` in function `chat_sdk.types.SlashCommandEvent.__init__`", - "severity": "error" - }, - { - "line": 866, - "column": 21, - "stop_line": 866, - "stop_column": 25, - "path": "src/chat_sdk/adapters/slack/adapter.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `None` is not assignable to parameter `channel` with type `Channel` in function `chat_sdk.types.SlashCommandEvent.__init__`\n Protocol `Channel` requires attribute `name`", - "concise_description": "Argument `None` is not assignable to parameter `channel` with type `Channel` in function `chat_sdk.types.SlashCommandEvent.__init__`", - "severity": "error" - }, - { - "line": 926, - "column": 25, - "stop_line": 926, - "stop_column": 29, - "path": "src/chat_sdk/adapters/slack/adapter.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `Self@SlackAdapter` is not assignable to parameter `adapter` with type `Adapter` in function `chat_sdk.types.ActionEvent.__init__`\n Property getter for `Self@SlackAdapter.lock_scope` has type `(self: Self@SlackAdapter) -> str`, which is not assignable to `(self: Self@SlackAdapter) -> Literal['channel', 'thread'] | None`, the property getter for `Adapter.lock_scope`", - "concise_description": "Argument `Self@SlackAdapter` is not assignable to parameter `adapter` with type `Adapter` in function `chat_sdk.types.ActionEvent.__init__`", - "severity": "error" - }, - { - "line": 980, - "column": 21, - "stop_line": 980, - "stop_column": 25, - "path": "src/chat_sdk/adapters/slack/adapter.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `Self@SlackAdapter` is not assignable to parameter `adapter` with type `Adapter` in function `chat_sdk.types.ModalSubmitEvent.__init__`\n Property getter for `Self@SlackAdapter.lock_scope` has type `(self: Self@SlackAdapter) -> str`, which is not assignable to `(self: Self@SlackAdapter) -> Literal['channel', 'thread'] | None`, the property getter for `Adapter.lock_scope`", - "concise_description": "Argument `Self@SlackAdapter` is not assignable to parameter `adapter` with type `Adapter` in function `chat_sdk.types.ModalSubmitEvent.__init__`", - "severity": "error" - }, - { - "line": 1016, - "column": 21, - "stop_line": 1016, - "stop_column": 25, - "path": "src/chat_sdk/adapters/slack/adapter.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `Self@SlackAdapter` is not assignable to parameter `adapter` with type `Adapter` in function `chat_sdk.types.ModalCloseEvent.__init__`\n Property getter for `Self@SlackAdapter.lock_scope` has type `(self: Self@SlackAdapter) -> str`, which is not assignable to `(self: Self@SlackAdapter) -> Literal['channel', 'thread'] | None`, the property getter for `Adapter.lock_scope`", - "concise_description": "Argument `Self@SlackAdapter` is not assignable to parameter `adapter` with type `Adapter` in function `chat_sdk.types.ModalCloseEvent.__init__`", - "severity": "error" - }, - { - "line": 1036, - "column": 44, - "stop_line": 1036, - "stop_column": 49, - "path": "src/chat_sdk/adapters/slack/adapter.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `dict[Any, Any]` is not assignable to parameter `modal` with type `ModalElement` in function `chat_sdk.adapters.slack.modals.modal_to_slack_view`", - "concise_description": "Argument `dict[Any, Any]` is not assignable to parameter `modal` with type `ModalElement` in function `chat_sdk.adapters.slack.modals.modal_to_slack_view`", - "severity": "error" - }, - { - "line": 1074, - "column": 36, - "stop_line": 1074, - "stop_column": 40, - "path": "src/chat_sdk/adapters/slack/adapter.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `Self@SlackAdapter` is not assignable to parameter `adapter` with type `Adapter` in function `chat_sdk.types.ChatInstance.process_message`\n Property getter for `Self@SlackAdapter.lock_scope` has type `(self: Self@SlackAdapter) -> str`, which is not assignable to `(self: Self@SlackAdapter) -> Literal['channel', 'thread'] | None`, the property getter for `Adapter.lock_scope`", - "concise_description": "Argument `Self@SlackAdapter` is not assignable to parameter `adapter` with type `Adapter` in function `chat_sdk.types.ChatInstance.process_message`", - "severity": "error" - }, - { - "line": 1144, - "column": 24, - "stop_line": 1144, - "stop_column": 28, - "path": "src/chat_sdk/adapters/slack/adapter.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `None` is not assignable to parameter `thread` with type `Thread` in function `chat_sdk.types.ReactionEvent.__init__`\n Protocol `Thread` requires attribute `channel_id`", - "concise_description": "Argument `None` is not assignable to parameter `thread` with type `Thread` in function `chat_sdk.types.ReactionEvent.__init__`", - "severity": "error" - }, - { - "line": 1146, - "column": 25, - "stop_line": 1146, - "stop_column": 29, - "path": "src/chat_sdk/adapters/slack/adapter.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `Self@SlackAdapter` is not assignable to parameter `adapter` with type `Adapter` in function `chat_sdk.types.ReactionEvent.__init__`\n Property getter for `Self@SlackAdapter.lock_scope` has type `(self: Self@SlackAdapter) -> str`, which is not assignable to `(self: Self@SlackAdapter) -> Literal['channel', 'thread'] | None`, the property getter for `Adapter.lock_scope`", - "concise_description": "Argument `Self@SlackAdapter` is not assignable to parameter `adapter` with type `Adapter` in function `chat_sdk.types.ReactionEvent.__init__`", - "severity": "error" - }, - { - "line": 1186, - "column": 25, - "stop_line": 1186, - "stop_column": 29, - "path": "src/chat_sdk/adapters/slack/adapter.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `Self@SlackAdapter` is not assignable to parameter `adapter` with type `Adapter` in function `chat_sdk.types.AssistantThreadStartedEvent.__init__`\n Property getter for `Self@SlackAdapter.lock_scope` has type `(self: Self@SlackAdapter) -> str`, which is not assignable to `(self: Self@SlackAdapter) -> Literal['channel', 'thread'] | None`, the property getter for `Adapter.lock_scope`", - "concise_description": "Argument `Self@SlackAdapter` is not assignable to parameter `adapter` with type `Adapter` in function `chat_sdk.types.AssistantThreadStartedEvent.__init__`", - "severity": "error" - }, - { - "line": 1221, - "column": 25, - "stop_line": 1221, - "stop_column": 29, - "path": "src/chat_sdk/adapters/slack/adapter.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `Self@SlackAdapter` is not assignable to parameter `adapter` with type `Adapter` in function `chat_sdk.types.AssistantContextChangedEvent.__init__`\n Property getter for `Self@SlackAdapter.lock_scope` has type `(self: Self@SlackAdapter) -> str`, which is not assignable to `(self: Self@SlackAdapter) -> Literal['channel', 'thread'] | None`, the property getter for `Adapter.lock_scope`", - "concise_description": "Argument `Self@SlackAdapter` is not assignable to parameter `adapter` with type `Adapter` in function `chat_sdk.types.AssistantContextChangedEvent.__init__`", - "severity": "error" - }, - { - "line": 1248, - "column": 25, - "stop_line": 1248, - "stop_column": 29, - "path": "src/chat_sdk/adapters/slack/adapter.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `Self@SlackAdapter` is not assignable to parameter `adapter` with type `Adapter` in function `chat_sdk.types.AppHomeOpenedEvent.__init__`\n Property getter for `Self@SlackAdapter.lock_scope` has type `(self: Self@SlackAdapter) -> str`, which is not assignable to `(self: Self@SlackAdapter) -> Literal['channel', 'thread'] | None`, the property getter for `Adapter.lock_scope`", - "concise_description": "Argument `Self@SlackAdapter` is not assignable to parameter `adapter` with type `Adapter` in function `chat_sdk.types.AppHomeOpenedEvent.__init__`", - "severity": "error" - }, - { - "line": 1262, - "column": 25, - "stop_line": 1262, - "stop_column": 29, - "path": "src/chat_sdk/adapters/slack/adapter.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `Self@SlackAdapter` is not assignable to parameter `adapter` with type `Adapter` in function `chat_sdk.types.MemberJoinedChannelEvent.__init__`\n Property getter for `Self@SlackAdapter.lock_scope` has type `(self: Self@SlackAdapter) -> str`, which is not assignable to `(self: Self@SlackAdapter) -> Literal['channel', 'thread'] | None`, the property getter for `Adapter.lock_scope`", - "concise_description": "Argument `Self@SlackAdapter` is not assignable to parameter `adapter` with type `Adapter` in function `chat_sdk.types.MemberJoinedChannelEvent.__init__`", - "severity": "error" - }, - { - "line": 1749, - "column": 86, - "stop_line": 1749, - "stop_column": 96, - "path": "src/chat_sdk/adapters/slack/adapter.py", - "code": -2, - "name": "bad-return", - "description": "Function declared to return `RawMessage`, but one or more paths are missing an explicit `return`", - "concise_description": "Function declared to return `RawMessage`, but one or more paths are missing an explicit `return`", - "severity": "error" - }, - { - "line": 1842, - "column": 10, - "stop_line": 1842, - "stop_column": 20, - "path": "src/chat_sdk/adapters/slack/adapter.py", - "code": -2, - "name": "bad-return", - "description": "Function declared to return `RawMessage`, but one or more paths are missing an explicit `return`", - "concise_description": "Function declared to return `RawMessage`, but one or more paths are missing an explicit `return`", - "severity": "error" - }, - { - "line": 2134, - "column": 10, - "stop_line": 2134, - "stop_column": 26, - "path": "src/chat_sdk/adapters/slack/adapter.py", - "code": -2, - "name": "bad-return", - "description": "Function declared to return `EphemeralMessage`, but one or more paths are missing an explicit `return`", - "concise_description": "Function declared to return `EphemeralMessage`, but one or more paths are missing an explicit `return`", - "severity": "error" - }, - { - "line": 2203, - "column": 10, - "stop_line": 2203, - "stop_column": 26, - "path": "src/chat_sdk/adapters/slack/adapter.py", - "code": -2, - "name": "bad-return", - "description": "Function declared to return `ScheduledMessage`, but one or more paths are missing an explicit `return`", - "concise_description": "Function declared to return `ScheduledMessage`, but one or more paths are missing an explicit `return`", - "severity": "error" - }, - { - "line": 2268, - "column": 46, - "stop_line": 2268, - "stop_column": 49, - "path": "src/chat_sdk/adapters/slack/adapter.py", - "code": -2, - "name": "bad-return", - "description": "Function declared to return `str`, but one or more paths are missing an explicit `return`", - "concise_description": "Function declared to return `str`, but one or more paths are missing an explicit `return`", - "severity": "error" - }, - { - "line": 2287, - "column": 106, - "stop_line": 2287, - "stop_column": 120, - "path": "src/chat_sdk/adapters/slack/adapter.py", - "code": -2, - "name": "bad-return", - "description": "Function declared to return `dict[str, str]`, but one or more paths are missing an explicit `return`", - "concise_description": "Function declared to return `dict[str, str]`, but one or more paths are missing an explicit `return`", - "severity": "error" - }, - { - "line": 2295, - "column": 36, - "stop_line": 2295, - "stop_column": 41, - "path": "src/chat_sdk/adapters/slack/adapter.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `dict[str, Any]` is not assignable to parameter `modal` with type `ModalElement` in function `chat_sdk.adapters.slack.modals.modal_to_slack_view`", - "concise_description": "Argument `dict[str, Any]` is not assignable to parameter `modal` with type `ModalElement` in function `chat_sdk.adapters.slack.modals.modal_to_slack_view`", - "severity": "error" - }, - { - "line": 2310, - "column": 74, - "stop_line": 2310, - "stop_column": 88, - "path": "src/chat_sdk/adapters/slack/adapter.py", - "code": -2, - "name": "bad-return", - "description": "Function declared to return `dict[str, str]`, but one or more paths are missing an explicit `return`", - "concise_description": "Function declared to return `dict[str, str]`, but one or more paths are missing an explicit `return`", - "severity": "error" - }, - { - "line": 2312, - "column": 36, - "stop_line": 2312, - "stop_column": 41, - "path": "src/chat_sdk/adapters/slack/adapter.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `dict[str, Any]` is not assignable to parameter `modal` with type `ModalElement` in function `chat_sdk.adapters.slack.modals.modal_to_slack_view`", - "concise_description": "Argument `dict[str, Any]` is not assignable to parameter `modal` with type `ModalElement` in function `chat_sdk.adapters.slack.modals.modal_to_slack_view`", - "severity": "error" - }, - { - "line": 2370, - "column": 92, - "stop_line": 2370, - "stop_column": 103, - "path": "src/chat_sdk/adapters/slack/adapter.py", - "code": -2, - "name": "bad-return", - "description": "Function declared to return `FetchResult`, but one or more paths are missing an explicit `return`", - "concise_description": "Function declared to return `FetchResult`, but one or more paths are missing an explicit `return`", - "severity": "error" - }, - { - "line": 2455, - "column": 53, - "stop_line": 2455, - "stop_column": 63, - "path": "src/chat_sdk/adapters/slack/adapter.py", - "code": -2, - "name": "bad-return", - "description": "Function declared to return `ThreadInfo`, but one or more paths are missing an explicit `return`", - "concise_description": "Function declared to return `ThreadInfo`, but one or more paths are missing an explicit `return`", - "severity": "error" - }, - { - "line": 2496, - "column": 101, - "stop_line": 2496, - "stop_column": 112, - "path": "src/chat_sdk/adapters/slack/adapter.py", - "code": -2, - "name": "bad-return", - "description": "Function declared to return `FetchResult`, but one or more paths are missing an explicit `return`", - "concise_description": "Function declared to return `FetchResult`, but one or more paths are missing an explicit `return`", - "severity": "error" - }, - { - "line": 2573, - "column": 97, - "stop_line": 2573, - "stop_column": 114, - "path": "src/chat_sdk/adapters/slack/adapter.py", - "code": -2, - "name": "bad-return", - "description": "Function declared to return `ListThreadsResult`, but one or more paths are missing an explicit `return`", - "concise_description": "Function declared to return `ListThreadsResult`, but one or more paths are missing an explicit `return`", - "severity": "error" - }, - { - "line": 2621, - "column": 60, - "stop_line": 2621, - "stop_column": 71, - "path": "src/chat_sdk/adapters/slack/adapter.py", - "code": -2, - "name": "bad-return", - "description": "Function declared to return `ChannelInfo`, but one or more paths are missing an explicit `return`", - "concise_description": "Function declared to return `ChannelInfo`, but one or more paths are missing an explicit `return`", - "severity": "error" - }, - { - "line": 40, - "column": 9, - "stop_line": 40, - "stop_column": 15, - "path": "src/chat_sdk/adapters/slack/format_converter.py", - "code": -2, - "name": "bad-param-name-override", - "description": "Class member `SlackFormatConverter.to_ast` overrides parent class `BaseFormatConverter` in an inconsistent manner\n Got parameter name `mrkdwn`, expected `platform_text`", - "concise_description": "Class member `SlackFormatConverter.to_ast` overrides parent class `BaseFormatConverter` in an inconsistent manner", - "severity": "error" - }, - { - "line": 97, - "column": 9, - "stop_line": 97, - "stop_column": 27, - "path": "src/chat_sdk/adapters/slack/format_converter.py", - "code": -2, - "name": "bad-param-name-override", - "description": "Class member `SlackFormatConverter.extract_plain_text` overrides parent class `BaseFormatConverter` in an inconsistent manner\n Got parameter name `mrkdwn`, expected `platform_text`", - "concise_description": "Class member `SlackFormatConverter.extract_plain_text` overrides parent class `BaseFormatConverter` in an inconsistent manner", - "severity": "error" - }, - { - "line": 351, - "column": 36, - "stop_line": 351, - "stop_column": 40, - "path": "src/chat_sdk/adapters/teams/adapter.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `Self@TeamsAdapter` is not assignable to parameter `adapter` with type `Adapter` in function `chat_sdk.types.ChatInstance.process_message`\n Property getter for `Self@TeamsAdapter.lock_scope` has type `(self: Self@TeamsAdapter) -> str | None`, which is not assignable to `(self: Self@TeamsAdapter) -> Literal['channel', 'thread'] | None`, the property getter for `Adapter.lock_scope`", - "concise_description": "Argument `Self@TeamsAdapter` is not assignable to parameter `adapter` with type `Adapter` in function `chat_sdk.types.ChatInstance.process_message`", - "severity": "error" - }, - { - "line": 417, - "column": 25, - "stop_line": 417, - "stop_column": 29, - "path": "src/chat_sdk/adapters/teams/adapter.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `Self@TeamsAdapter` is not assignable to parameter `adapter` with type `Adapter` in function `chat_sdk.types.ActionEvent.__init__`\n Property getter for `Self@TeamsAdapter.lock_scope` has type `(self: Self@TeamsAdapter) -> str | None`, which is not assignable to `(self: Self@TeamsAdapter) -> Literal['channel', 'thread'] | None`, the property getter for `Adapter.lock_scope`", - "concise_description": "Argument `Self@TeamsAdapter` is not assignable to parameter `adapter` with type `Adapter` in function `chat_sdk.types.ActionEvent.__init__`", - "severity": "error" - }, - { - "line": 460, - "column": 25, - "stop_line": 460, - "stop_column": 29, - "path": "src/chat_sdk/adapters/teams/adapter.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `Self@TeamsAdapter` is not assignable to parameter `adapter` with type `Adapter` in function `chat_sdk.types.ActionEvent.__init__`\n Property getter for `Self@TeamsAdapter.lock_scope` has type `(self: Self@TeamsAdapter) -> str | None`, which is not assignable to `(self: Self@TeamsAdapter) -> Literal['channel', 'thread'] | None`, the property getter for `Adapter.lock_scope`", - "concise_description": "Argument `Self@TeamsAdapter` is not assignable to parameter `adapter` with type `Adapter` in function `chat_sdk.types.ActionEvent.__init__`", - "severity": "error" - }, - { - "line": 506, - "column": 28, - "stop_line": 506, - "stop_column": 32, - "path": "src/chat_sdk/adapters/teams/adapter.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `None` is not assignable to parameter `thread` with type `Thread` in function `chat_sdk.types.ReactionEvent.__init__`\n Protocol `Thread` requires attribute `channel_id`", - "concise_description": "Argument `None` is not assignable to parameter `thread` with type `Thread` in function `chat_sdk.types.ReactionEvent.__init__`", - "severity": "error" - }, - { - "line": 507, - "column": 29, - "stop_line": 507, - "stop_column": 33, - "path": "src/chat_sdk/adapters/teams/adapter.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `Self@TeamsAdapter` is not assignable to parameter `adapter` with type `Adapter` in function `chat_sdk.types.ReactionEvent.__init__`\n Property getter for `Self@TeamsAdapter.lock_scope` has type `(self: Self@TeamsAdapter) -> str | None`, which is not assignable to `(self: Self@TeamsAdapter) -> Literal['channel', 'thread'] | None`, the property getter for `Adapter.lock_scope`", - "concise_description": "Argument `Self@TeamsAdapter` is not assignable to parameter `adapter` with type `Adapter` in function `chat_sdk.types.ReactionEvent.__init__`", - "severity": "error" - }, - { - "line": 523, - "column": 28, - "stop_line": 523, - "stop_column": 32, - "path": "src/chat_sdk/adapters/teams/adapter.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `None` is not assignable to parameter `thread` with type `Thread` in function `chat_sdk.types.ReactionEvent.__init__`\n Protocol `Thread` requires attribute `channel_id`", - "concise_description": "Argument `None` is not assignable to parameter `thread` with type `Thread` in function `chat_sdk.types.ReactionEvent.__init__`", - "severity": "error" - }, - { - "line": 524, - "column": 29, - "stop_line": 524, - "stop_column": 33, - "path": "src/chat_sdk/adapters/teams/adapter.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `Self@TeamsAdapter` is not assignable to parameter `adapter` with type `Adapter` in function `chat_sdk.types.ReactionEvent.__init__`\n Property getter for `Self@TeamsAdapter.lock_scope` has type `(self: Self@TeamsAdapter) -> str | None`, which is not assignable to `(self: Self@TeamsAdapter) -> Literal['channel', 'thread'] | None`, the property getter for `Adapter.lock_scope`", - "concise_description": "Argument `Self@TeamsAdapter` is not assignable to parameter `adapter` with type `Adapter` in function `chat_sdk.types.ReactionEvent.__init__`", - "severity": "error" - }, - { - "line": 582, - "column": 18, - "stop_line": 582, - "stop_column": 26, - "path": "src/chat_sdk/adapters/teams/adapter.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `str` is not assignable to parameter `type` with type `Literal['audio', 'file', 'image', 'video']` in function `chat_sdk.types.Attachment.__init__`", - "concise_description": "Argument `str` is not assignable to parameter `type` with type `Literal['audio', 'file', 'image', 'video']` in function `chat_sdk.types.Attachment.__init__`", - "severity": "error" - }, - { - "line": 1781, - "column": 24, - "stop_line": 1781, - "stop_column": 44, - "path": "src/chat_sdk/adapters/teams/adapter.py", - "code": -2, - "name": "not-async", - "description": "Type `object` is not awaitable", - "concise_description": "Type `object` is not awaitable", - "severity": "error" - }, - { - "line": 138, - "column": 48, - "stop_line": 138, - "stop_column": 53, - "path": "src/chat_sdk/adapters/teams/cards.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `object | str` is not assignable to parameter `text` with type `str` in function `_convert_emoji`", - "concise_description": "Argument `object | str` is not assignable to parameter `text` with type `str` in function `_convert_emoji`", - "severity": "error" - }, - { - "line": 674, - "column": 36, - "stop_line": 674, - "stop_column": 40, - "path": "src/chat_sdk/adapters/telegram/adapter.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `Self@TelegramAdapter` is not assignable to parameter `adapter` with type `Adapter` in function `chat_sdk.types.ChatInstance.process_message`\n Property getter for `Self@TelegramAdapter.lock_scope` has type `(self: Self@TelegramAdapter) -> str`, which is not assignable to `(self: Self@TelegramAdapter) -> Literal['channel', 'thread'] | None`, the property getter for `Adapter.lock_scope`", - "concise_description": "Argument `Self@TelegramAdapter` is not assignable to parameter `adapter` with type `Adapter` in function `chat_sdk.types.ChatInstance.process_message`", - "severity": "error" - }, - { - "line": 705, - "column": 28, - "stop_line": 705, - "stop_column": 37, - "path": "src/chat_sdk/adapters/telegram/adapter.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `object | TelegramUser` is not assignable to parameter `user` with type `TelegramUser` in function `TelegramAdapter.to_author`", - "concise_description": "Argument `object | TelegramUser` is not assignable to parameter `user` with type `TelegramUser` in function `TelegramAdapter.to_author`", - "severity": "error" - }, - { - "line": 718, - "column": 25, - "stop_line": 718, - "stop_column": 29, - "path": "src/chat_sdk/adapters/telegram/adapter.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `Self@TelegramAdapter` is not assignable to parameter `adapter` with type `Adapter` in function `chat_sdk.types.ActionEvent.__init__`\n Property getter for `Self@TelegramAdapter.lock_scope` has type `(self: Self@TelegramAdapter) -> str`, which is not assignable to `(self: Self@TelegramAdapter) -> Literal['channel', 'thread'] | None`, the property getter for `Adapter.lock_scope`", - "concise_description": "Argument `Self@TelegramAdapter` is not assignable to parameter `adapter` with type `Adapter` in function `chat_sdk.types.ActionEvent.__init__`", - "severity": "error" - }, - { - "line": 723, - "column": 27, - "stop_line": 723, - "stop_column": 36, - "path": "src/chat_sdk/adapters/telegram/adapter.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `str | None` is not assignable to parameter `action_id` with type `str` in function `chat_sdk.types.ActionEvent.__init__`", - "concise_description": "Argument `str | None` is not assignable to parameter `action_id` with type `str` in function `chat_sdk.types.ActionEvent.__init__`", - "severity": "error" - }, - { - "line": 783, - "column": 33, - "stop_line": 783, - "stop_column": 37, - "path": "src/chat_sdk/adapters/telegram/adapter.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `Self@TelegramAdapter` is not assignable to parameter `adapter` with type `Adapter` in function `chat_sdk.types.ReactionEvent.__init__`\n Property getter for `Self@TelegramAdapter.lock_scope` has type `(self: Self@TelegramAdapter) -> str`, which is not assignable to `(self: Self@TelegramAdapter) -> Literal['channel', 'thread'] | None`, the property getter for `Adapter.lock_scope`", - "concise_description": "Argument `Self@TelegramAdapter` is not assignable to parameter `adapter` with type `Adapter` in function `chat_sdk.types.ReactionEvent.__init__`", - "severity": "error" - }, - { - "line": 784, - "column": 32, - "stop_line": 784, - "stop_column": 36, - "path": "src/chat_sdk/adapters/telegram/adapter.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `None` is not assignable to parameter `thread` with type `Thread` in function `chat_sdk.types.ReactionEvent.__init__`\n Protocol `Thread` requires attribute `channel_id`", - "concise_description": "Argument `None` is not assignable to parameter `thread` with type `Thread` in function `chat_sdk.types.ReactionEvent.__init__`", - "severity": "error" - }, - { - "line": 801, - "column": 33, - "stop_line": 801, - "stop_column": 37, - "path": "src/chat_sdk/adapters/telegram/adapter.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `Self@TelegramAdapter` is not assignable to parameter `adapter` with type `Adapter` in function `chat_sdk.types.ReactionEvent.__init__`\n Property getter for `Self@TelegramAdapter.lock_scope` has type `(self: Self@TelegramAdapter) -> str`, which is not assignable to `(self: Self@TelegramAdapter) -> Literal['channel', 'thread'] | None`, the property getter for `Adapter.lock_scope`", - "concise_description": "Argument `Self@TelegramAdapter` is not assignable to parameter `adapter` with type `Adapter` in function `chat_sdk.types.ReactionEvent.__init__`", - "severity": "error" - }, - { - "line": 802, - "column": 32, - "stop_line": 802, - "stop_column": 36, - "path": "src/chat_sdk/adapters/telegram/adapter.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `None` is not assignable to parameter `thread` with type `Thread` in function `chat_sdk.types.ReactionEvent.__init__`\n Protocol `Thread` requires attribute `channel_id`", - "concise_description": "Argument `None` is not assignable to parameter `thread` with type `Thread` in function `chat_sdk.types.ReactionEvent.__init__`", - "severity": "error" - }, - { - "line": 1239, - "column": 37, - "stop_line": 1239, - "stop_column": 46, - "path": "src/chat_sdk/adapters/telegram/adapter.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `object | TelegramUser` is not assignable to parameter `user` with type `TelegramUser` in function `TelegramAdapter.to_author`", - "concise_description": "Argument `object | TelegramUser` is not assignable to parameter `user` with type `TelegramUser` in function `TelegramAdapter.to_author`", - "severity": "error" - }, - { - "line": 1728, - "column": 20, - "stop_line": 1728, - "stop_column": 45, - "path": "src/chat_sdk/adapters/telegram/adapter.py", - "code": -2, - "name": "bad-return", - "description": "Returned type `object | str` is not assignable to declared return type `str`", - "concise_description": "Returned type `object | str` is not assignable to declared return type `str`", - "severity": "error" - }, - { - "line": 1734, - "column": 30, - "stop_line": 1734, - "stop_column": 55, - "path": "src/chat_sdk/adapters/telegram/adapter.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `object | str` is not assignable to parameter `name` with type `str` in function `chat_sdk.emoji.get_emoji`", - "concise_description": "Argument `object | str` is not assignable to parameter `name` with type `str` in function `chat_sdk.emoji.get_emoji`", - "severity": "error" - }, - { - "line": 1913, - "column": 20, - "stop_line": 1913, - "stop_column": 40, - "path": "src/chat_sdk/adapters/telegram/adapter.py", - "code": -2, - "name": "not-async", - "description": "Type `object` is not awaitable", - "concise_description": "Type `object` is not awaitable", - "severity": "error" - }, - { - "line": 1917, - "column": 24, - "stop_line": 1917, - "stop_column": 36, - "path": "src/chat_sdk/adapters/telegram/adapter.py", - "code": -2, - "name": "not-async", - "description": "Type `object` is not awaitable", - "concise_description": "Type `object` is not awaitable", - "severity": "error" - }, - { - "line": 53, - "column": 9, - "stop_line": 53, - "stop_column": 15, - "path": "src/chat_sdk/adapters/telegram/format_converter.py", - "code": -2, - "name": "bad-param-name-override", - "description": "Class member `TelegramFormatConverter.to_ast` overrides parent class `BaseFormatConverter` in an inconsistent manner\n Got parameter name `text`, expected `platform_text`", - "concise_description": "Class member `TelegramFormatConverter.to_ast` overrides parent class `BaseFormatConverter` in an inconsistent manner", - "severity": "error" - }, - { - "line": 210, - "column": 33, - "stop_line": 210, - "stop_column": 40, - "path": "src/chat_sdk/adapters/whatsapp/adapter.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `dict[str, Any]` is not assignable to parameter `inbound` with type `WhatsAppInboundMessage` in function `WhatsAppAdapter._handle_inbound_message`", - "concise_description": "Argument `dict[str, Any]` is not assignable to parameter `inbound` with type `WhatsAppInboundMessage` in function `WhatsAppAdapter._handle_inbound_message`", - "severity": "error" - }, - { - "line": 310, - "column": 36, - "stop_line": 310, - "stop_column": 42, - "path": "src/chat_sdk/adapters/whatsapp/adapter.py", - "code": -2, - "name": "bad-typed-dict-key", - "description": "TypedDict `WhatsAppInboundMessage` does not have key `from`\n Did you mean `from_`?", - "concise_description": "TypedDict `WhatsAppInboundMessage` does not have key `from`", - "severity": "error" - }, - { - "line": 315, - "column": 36, - "stop_line": 315, - "stop_column": 40, - "path": "src/chat_sdk/adapters/whatsapp/adapter.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `Self@WhatsAppAdapter` is not assignable to parameter `adapter` with type `Adapter` in function `chat_sdk.types.ChatInstance.process_message`\n Property getter for `Self@WhatsAppAdapter.lock_scope` has type `(self: Self@WhatsAppAdapter) -> str`, which is not assignable to `(self: Self@WhatsAppAdapter) -> Literal['channel', 'thread'] | None`, the property getter for `Adapter.lock_scope`", - "concise_description": "Argument `Self@WhatsAppAdapter` is not assignable to parameter `adapter` with type `Adapter` in function `chat_sdk.types.ChatInstance.process_message`", - "severity": "error" - }, - { - "line": 331, - "column": 36, - "stop_line": 331, - "stop_column": 42, - "path": "src/chat_sdk/adapters/whatsapp/adapter.py", - "code": -2, - "name": "bad-typed-dict-key", - "description": "TypedDict `WhatsAppInboundMessage` does not have key `from`\n Did you mean `from_`?", - "concise_description": "TypedDict `WhatsAppInboundMessage` does not have key `from`", - "severity": "error" - }, - { - "line": 339, - "column": 86, - "stop_line": 339, - "stop_column": 92, - "path": "src/chat_sdk/adapters/whatsapp/adapter.py", - "code": -2, - "name": "bad-typed-dict-key", - "description": "TypedDict `WhatsAppInboundMessage` does not have key `from`\n Did you mean `from_`?", - "concise_description": "TypedDict `WhatsAppInboundMessage` does not have key `from`", - "severity": "error" - }, - { - "line": 341, - "column": 29, - "stop_line": 341, - "stop_column": 35, - "path": "src/chat_sdk/adapters/whatsapp/adapter.py", - "code": -2, - "name": "bad-typed-dict-key", - "description": "TypedDict `WhatsAppInboundMessage` does not have key `from`\n Did you mean `from_`?", - "concise_description": "TypedDict `WhatsAppInboundMessage` does not have key `from`", - "severity": "error" - }, - { - "line": 350, - "column": 25, - "stop_line": 350, - "stop_column": 29, - "path": "src/chat_sdk/adapters/whatsapp/adapter.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `Self@WhatsAppAdapter` is not assignable to parameter `adapter` with type `Adapter` in function `chat_sdk.types.ReactionEvent.__init__`\n Property getter for `Self@WhatsAppAdapter.lock_scope` has type `(self: Self@WhatsAppAdapter) -> str`, which is not assignable to `(self: Self@WhatsAppAdapter) -> Literal['channel', 'thread'] | None`, the property getter for `Adapter.lock_scope`", - "concise_description": "Argument `Self@WhatsAppAdapter` is not assignable to parameter `adapter` with type `Adapter` in function `chat_sdk.types.ReactionEvent.__init__`", - "severity": "error" - }, - { - "line": 351, - "column": 24, - "stop_line": 351, - "stop_column": 28, - "path": "src/chat_sdk/adapters/whatsapp/adapter.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `None` is not assignable to parameter `thread` with type `Thread` in function `chat_sdk.types.ReactionEvent.__init__`\n Protocol `Thread` requires attribute `channel_id`", - "concise_description": "Argument `None` is not assignable to parameter `thread` with type `Thread` in function `chat_sdk.types.ReactionEvent.__init__`", - "severity": "error" - }, - { - "line": 377, - "column": 36, - "stop_line": 377, - "stop_column": 42, - "path": "src/chat_sdk/adapters/whatsapp/adapter.py", - "code": -2, - "name": "bad-typed-dict-key", - "description": "TypedDict `WhatsAppInboundMessage` does not have key `from`\n Did you mean `from_`?", - "concise_description": "TypedDict `WhatsAppInboundMessage` does not have key `from`", - "severity": "error" - }, - { - "line": 398, - "column": 86, - "stop_line": 398, - "stop_column": 92, - "path": "src/chat_sdk/adapters/whatsapp/adapter.py", - "code": -2, - "name": "bad-typed-dict-key", - "description": "TypedDict `WhatsAppInboundMessage` does not have key `from`\n Did you mean `from_`?", - "concise_description": "TypedDict `WhatsAppInboundMessage` does not have key `from`", - "severity": "error" - }, - { - "line": 401, - "column": 25, - "stop_line": 401, - "stop_column": 29, - "path": "src/chat_sdk/adapters/whatsapp/adapter.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `Self@WhatsAppAdapter` is not assignable to parameter `adapter` with type `Adapter` in function `chat_sdk.types.ActionEvent.__init__`\n Property getter for `Self@WhatsAppAdapter.lock_scope` has type `(self: Self@WhatsAppAdapter) -> str`, which is not assignable to `(self: Self@WhatsAppAdapter) -> Literal['channel', 'thread'] | None`, the property getter for `Adapter.lock_scope`", - "concise_description": "Argument `Self@WhatsAppAdapter` is not assignable to parameter `adapter` with type `Adapter` in function `chat_sdk.types.ActionEvent.__init__`", - "severity": "error" - }, - { - "line": 406, - "column": 37, - "stop_line": 406, - "stop_column": 43, - "path": "src/chat_sdk/adapters/whatsapp/adapter.py", - "code": -2, - "name": "bad-typed-dict-key", - "description": "TypedDict `WhatsAppInboundMessage` does not have key `from`\n Did you mean `from_`?", - "concise_description": "TypedDict `WhatsAppInboundMessage` does not have key `from`", - "severity": "error" - }, - { - "line": 412, - "column": 27, - "stop_line": 412, - "stop_column": 36, - "path": "src/chat_sdk/adapters/whatsapp/adapter.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `str | None` is not assignable to parameter `action_id` with type `str` in function `chat_sdk.types.ActionEvent.__init__`", - "concise_description": "Argument `str | None` is not assignable to parameter `action_id` with type `str` in function `chat_sdk.types.ActionEvent.__init__`", - "severity": "error" - }, - { - "line": 433, - "column": 36, - "stop_line": 433, - "stop_column": 42, - "path": "src/chat_sdk/adapters/whatsapp/adapter.py", - "code": -2, - "name": "bad-typed-dict-key", - "description": "TypedDict `WhatsAppInboundMessage` does not have key `from`\n Did you mean `from_`?", - "concise_description": "TypedDict `WhatsAppInboundMessage` does not have key `from`", - "severity": "error" - }, - { - "line": 437, - "column": 86, - "stop_line": 437, - "stop_column": 92, - "path": "src/chat_sdk/adapters/whatsapp/adapter.py", - "code": -2, - "name": "bad-typed-dict-key", - "description": "TypedDict `WhatsAppInboundMessage` does not have key `from`\n Did you mean `from_`?", - "concise_description": "TypedDict `WhatsAppInboundMessage` does not have key `from`", - "severity": "error" - }, - { - "line": 440, - "column": 25, - "stop_line": 440, - "stop_column": 29, - "path": "src/chat_sdk/adapters/whatsapp/adapter.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `Self@WhatsAppAdapter` is not assignable to parameter `adapter` with type `Adapter` in function `chat_sdk.types.ActionEvent.__init__`\n Property getter for `Self@WhatsAppAdapter.lock_scope` has type `(self: Self@WhatsAppAdapter) -> str`, which is not assignable to `(self: Self@WhatsAppAdapter) -> Literal['channel', 'thread'] | None`, the property getter for `Adapter.lock_scope`", - "concise_description": "Argument `Self@WhatsAppAdapter` is not assignable to parameter `adapter` with type `Adapter` in function `chat_sdk.types.ActionEvent.__init__`", - "severity": "error" - }, - { - "line": 445, - "column": 37, - "stop_line": 445, - "stop_column": 43, - "path": "src/chat_sdk/adapters/whatsapp/adapter.py", - "code": -2, - "name": "bad-typed-dict-key", - "description": "TypedDict `WhatsAppInboundMessage` does not have key `from`\n Did you mean `from_`?", - "concise_description": "TypedDict `WhatsAppInboundMessage` does not have key `from`", - "severity": "error" - }, - { - "line": 499, - "column": 86, - "stop_line": 499, - "stop_column": 92, - "path": "src/chat_sdk/adapters/whatsapp/adapter.py", - "code": -2, - "name": "bad-typed-dict-key", - "description": "TypedDict `WhatsAppInboundMessage` does not have key `from`\n Did you mean `from_`?", - "concise_description": "TypedDict `WhatsAppInboundMessage` does not have key `from`", - "severity": "error" - }, - { - "line": 501, - "column": 29, - "stop_line": 501, - "stop_column": 35, - "path": "src/chat_sdk/adapters/whatsapp/adapter.py", - "code": -2, - "name": "bad-typed-dict-key", - "description": "TypedDict `WhatsAppInboundMessage` does not have key `from`\n Did you mean `from_`?", - "concise_description": "TypedDict `WhatsAppInboundMessage` does not have key `from`", - "severity": "error" - }, - { - "line": 511, - "column": 24, - "stop_line": 511, - "stop_column": 31, - "path": "src/chat_sdk/adapters/whatsapp/adapter.py", - "code": -2, - "name": "bad-typed-dict-key", - "description": "`WhatsAppInboundMessage` is not assignable to TypedDict key `message` with type `dict[str, Any]`", - "concise_description": "`WhatsAppInboundMessage` is not assignable to TypedDict key `message` with type `dict[str, Any]`", - "severity": "error" - }, - { - "line": 713, - "column": 87, - "stop_line": 713, - "stop_column": 100, - "path": "src/chat_sdk/adapters/whatsapp/adapter.py", - "code": -2, - "name": "bad-typed-dict-key", - "description": "TypedDict `WhatsAppCardResultText` does not have key `interactive`", - "concise_description": "TypedDict `WhatsAppCardResultText` does not have key `interactive`", - "severity": "error" - }, - { - "line": 718, - "column": 51, - "stop_line": 718, - "stop_column": 57, - "path": "src/chat_sdk/adapters/whatsapp/adapter.py", - "code": -2, - "name": "bad-typed-dict-key", - "description": "TypedDict `WhatsAppCardResultInteractive` does not have key `text`", - "concise_description": "TypedDict `WhatsAppCardResultInteractive` does not have key `text`", - "severity": "error" - }, - { - "line": 843, - "column": 51, - "stop_line": 843, - "stop_column": 76, - "path": "src/chat_sdk/adapters/whatsapp/adapter.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `dict[str, str]` is not assignable to parameter `message` with type `PostableAst | PostableCard | PostableMarkdown | PostableRaw | str | CardElement` in function `WhatsAppAdapter.post_message`", - "concise_description": "Argument `dict[str, str]` is not assignable to parameter `message` with type `PostableAst | PostableCard | PostableMarkdown | PostableRaw | str | CardElement` in function `WhatsAppAdapter.post_message`", - "severity": "error" - }, - { - "line": 953, - "column": 43, - "stop_line": 953, - "stop_column": 57, - "path": "src/chat_sdk/adapters/whatsapp/adapter.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `dict[str, Any]` is not assignable to parameter `message` with type `WhatsAppInboundMessage` in function `WhatsAppAdapter._extract_text_content`", - "concise_description": "Argument `dict[str, Any]` is not assignable to parameter `message` with type `WhatsAppInboundMessage` in function `WhatsAppAdapter._extract_text_content`", - "severity": "error" - }, - { - "line": 955, - "column": 47, - "stop_line": 955, - "stop_column": 61, - "path": "src/chat_sdk/adapters/whatsapp/adapter.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `dict[str, Any]` is not assignable to parameter `inbound` with type `WhatsAppInboundMessage` in function `WhatsAppAdapter._build_attachments`", - "concise_description": "Argument `dict[str, Any]` is not assignable to parameter `inbound` with type `WhatsAppInboundMessage` in function `WhatsAppAdapter._build_attachments`", - "severity": "error" - }, - { - "line": 1044, - "column": 20, - "stop_line": 1044, - "stop_column": 40, - "path": "src/chat_sdk/adapters/whatsapp/adapter.py", - "code": -2, - "name": "not-async", - "description": "Type `object` is not awaitable", - "concise_description": "Type `object` is not awaitable", - "severity": "error" - }, - { - "line": 231, - "column": 16, - "stop_line": 231, - "stop_column": 21, - "path": "src/chat_sdk/adapters/whatsapp/cards.py", - "code": -2, - "name": "bad-return", - "description": "Returned type `list[object | str]` is not assignable to declared return type `list[str]`", - "concise_description": "Returned type `list[object | str]` is not assignable to declared return type `list[str]`", - "severity": "error" - }, - { - "line": 82, - "column": 9, - "stop_line": 82, - "stop_column": 15, - "path": "src/chat_sdk/adapters/whatsapp/format_converter.py", - "code": -2, - "name": "bad-param-name-override", - "description": "Class member `WhatsAppFormatConverter.to_ast` overrides parent class `BaseFormatConverter` in an inconsistent manner\n Got parameter name `markdown`, expected `platform_text`", - "concise_description": "Class member `WhatsAppFormatConverter.to_ast` overrides parent class `BaseFormatConverter` in an inconsistent manner", - "severity": "error" - }, - { - "line": 772, - "column": 61, - "stop_line": 772, - "stop_column": 65, - "path": "src/chat_sdk/chat.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `Self@Chat` is not assignable to parameter `chat` with type `_ChatSingleton | None` in function `chat_sdk.thread.ThreadImpl.from_json`", - "concise_description": "Argument `Self@Chat` is not assignable to parameter `chat` with type `_ChatSingleton | None` in function `chat_sdk.thread.ThreadImpl.from_json`", - "severity": "error" - }, - { - "line": 774, - "column": 62, - "stop_line": 774, - "stop_column": 66, - "path": "src/chat_sdk/chat.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `Self@Chat` is not assignable to parameter `chat` with type `_ChatSingleton | None` in function `chat_sdk.channel.ChannelImpl.from_json`", - "concise_description": "Argument `Self@Chat` is not assignable to parameter `chat` with type `_ChatSingleton | None` in function `chat_sdk.channel.ChannelImpl.from_json`", - "severity": "error" - }, - { - "line": 1049, - "column": 13, - "stop_line": 1053, - "stop_column": 14, - "path": "src/chat_sdk/chat.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `_ChannelImplConfigForChat` is not assignable to parameter `config` with type `_ChannelImplConfigForThread | _ChannelImplConfigLazy | _ChannelImplConfigWithAdapter` in function `chat_sdk.channel.ChannelImpl.__init__`", - "concise_description": "Argument `_ChannelImplConfigForChat` is not assignable to parameter `config` with type `_ChannelImplConfigForThread | _ChannelImplConfigLazy | _ChannelImplConfigWithAdapter` in function `chat_sdk.channel.ChannelImpl.__init__`", - "severity": "error" - }, - { - "line": 1071, - "column": 21, - "stop_line": 1071, - "stop_column": 28, - "path": "src/chat_sdk/chat.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `ChannelImpl` is not assignable to parameter `channel` with type `Channel` in function `chat_sdk.types.SlashCommandEvent.__init__`\n `ChannelImpl.set_state` has type `(self: ChannelImpl, new_state: dict[str, Any], *, replace: bool = False) -> Coroutine[Unknown, Unknown, None]`, which is not assignable to `(self: ChannelImpl, state: dict[str, Any], *, replace: bool = False) -> Coroutine[Unknown, Unknown, None]`, the type of `Channel.set_state`", - "concise_description": "Argument `ChannelImpl` is not assignable to parameter `channel` with type `Channel` in function `chat_sdk.types.SlashCommandEvent.__init__`", - "severity": "error" - }, - { - "line": 1244, - "column": 20, - "stop_line": 1244, - "stop_column": 26, - "path": "src/chat_sdk/chat.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `ThreadImpl | None` is not assignable to parameter `thread` with type `Thread | None` in function `chat_sdk.types.ActionEvent.__init__`", - "concise_description": "Argument `ThreadImpl | None` is not assignable to parameter `thread` with type `Thread | None` in function `chat_sdk.types.ActionEvent.__init__`", - "severity": "error" - }, - { - "line": 1306, - "column": 20, - "stop_line": 1306, - "stop_column": 26, - "path": "src/chat_sdk/chat.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `ThreadImpl` is not assignable to parameter `thread` with type `Thread` in function `chat_sdk.types.ReactionEvent.__init__`\n Property getter for `ThreadImpl.channel` has type `(self: ThreadImpl) -> ChannelImpl`, which is not assignable to `(self: ThreadImpl) -> Channel`, the property getter for `Thread.channel`", - "concise_description": "Argument `ThreadImpl` is not assignable to parameter `thread` with type `Thread` in function `chat_sdk.types.ReactionEvent.__init__`", - "severity": "error" - }, - { - "line": 1374, - "column": 13, - "stop_line": 1378, - "stop_column": 14, - "path": "src/chat_sdk/chat.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `_ChannelImplConfigForChat` is not assignable to parameter `config` with type `_ChannelImplConfigForThread | _ChannelImplConfigLazy | _ChannelImplConfigWithAdapter` in function `chat_sdk.channel.ChannelImpl.__init__`", - "concise_description": "Argument `_ChannelImplConfigForChat` is not assignable to parameter `config` with type `_ChannelImplConfigForThread | _ChannelImplConfigLazy | _ChannelImplConfigWithAdapter` in function `chat_sdk.channel.ChannelImpl.__init__`", - "severity": "error" - }, - { - "line": 1429, - "column": 21, - "stop_line": 1436, - "stop_column": 14, - "path": "src/chat_sdk/chat.py", - "code": -2, - "name": "not-async", - "description": "Type `Literal['channel']` is not awaitable", - "concise_description": "Type `Literal['channel']` is not awaitable", - "severity": "error" - }, - { - "line": 1429, - "column": 21, - "stop_line": 1436, - "stop_column": 14, - "path": "src/chat_sdk/chat.py", - "code": -2, - "name": "not-async", - "description": "Type `Literal['thread']` is not awaitable", - "concise_description": "Type `Literal['thread']` is not awaitable", - "severity": "error" - }, - { - "line": 1793, - "column": 17, - "stop_line": 1793, - "stop_column": 59, - "path": "src/chat_sdk/chat.py", - "code": -2, - "name": "not-async", - "description": "Type `None` is not awaitable", - "concise_description": "Type `None` is not awaitable", - "severity": "error" - }, - { - "line": 1818, - "column": 17, - "stop_line": 1818, - "stop_column": 60, - "path": "src/chat_sdk/chat.py", - "code": -2, - "name": "not-async", - "description": "Type `None` is not awaitable", - "concise_description": "Type `None` is not awaitable", - "severity": "error" - }, - { - "line": 194, - "column": 50, - "stop_line": 194, - "stop_column": 57, - "path": "src/chat_sdk/shared/base_format_converter.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `dict[Any, Any]` is not assignable to parameter `card` with type `CardElement` in function `chat_sdk.cards.card_to_fallback_text`", - "concise_description": "Argument `dict[Any, Any]` is not assignable to parameter `card` with type `CardElement` in function `chat_sdk.cards.card_to_fallback_text`", - "severity": "error" - }, - { - "line": 68, - "column": 29, - "stop_line": 68, - "stop_column": 53, - "path": "src/chat_sdk/shared/card_utils.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `object | str` is not assignable to parameter with type `str`", - "concise_description": "Argument `object | str` is not assignable to parameter with type `str`", - "severity": "error" - }, - { - "line": 70, - "column": 32, - "stop_line": 70, - "stop_column": 54, - "path": "src/chat_sdk/shared/card_utils.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `object | str` is not assignable to parameter with type `str`", - "concise_description": "Argument `object | str` is not assignable to parameter with type `str`", - "severity": "error" - }, - { - "line": 72, - "column": 93, - "stop_line": 72, - "stop_column": 118, - "path": "src/chat_sdk/shared/card_utils.py", - "code": -2, - "name": "not-iterable", - "description": "Type `object` is not iterable", - "concise_description": "Type `object` is not iterable", - "severity": "error" - }, - { - "line": 76, - "column": 90, - "stop_line": 76, - "stop_column": 115, - "path": "src/chat_sdk/shared/card_utils.py", - "code": -2, - "name": "not-iterable", - "description": "Type `object` is not iterable", - "concise_description": "Type `object` is not iterable", - "severity": "error" - }, - { - "line": 78, - "column": 39, - "stop_line": 78, - "stop_column": 63, - "path": "src/chat_sdk/shared/card_utils.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `list[str] | object` is not assignable to parameter `headers` with type `list[str]` in function `chat_sdk.cards.table_element_to_ascii`", - "concise_description": "Argument `list[str] | object` is not assignable to parameter `headers` with type `list[str]` in function `chat_sdk.cards.table_element_to_ascii`", - "severity": "error" - }, - { - "line": 78, - "column": 65, - "stop_line": 78, - "stop_column": 86, - "path": "src/chat_sdk/shared/card_utils.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `list[list[str]] | object` is not assignable to parameter `rows` with type `list[list[str]]` in function `chat_sdk.cards.table_element_to_ascii`", - "concise_description": "Argument `list[list[str]] | object` is not assignable to parameter `rows` with type `list[list[str]]` in function `chat_sdk.cards.table_element_to_ascii`", - "severity": "error" - }, - { - "line": 193, - "column": 49, - "stop_line": 193, - "stop_column": 53, - "path": "src/chat_sdk/shared/mock_adapter.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `None` is not assignable to parameter `thread_id` with type `str` in function `chat_sdk.types.RawMessage.__init__`", - "concise_description": "Argument `None` is not assignable to parameter `thread_id` with type `str` in function `chat_sdk.types.RawMessage.__init__`", - "severity": "error" - }, - { - "line": 109, - "column": 20, - "stop_line": 109, - "stop_column": 45, - "path": "src/chat_sdk/state/redis.py", - "code": -2, - "name": "missing-import", - "description": "Cannot find module `redis.asyncio`\n Looked in these locations (from config in `/home/user/chat-sdk-python/pyproject.toml`):\n Import root (inferred from project layout): \"/home/user/chat-sdk-python/src\"\n Site package path queried from interpreter: [\"/home/user/chat-sdk-python/.venv/lib/python3.11/site-packages\", \"/home/user/chat-sdk-python/src\"]", - "concise_description": "Cannot find module `redis.asyncio`", - "severity": "error" - }, - { - "line": 578, - "column": 27, - "stop_line": 578, - "stop_column": 37, - "path": "src/chat_sdk/thread.py", - "code": -2, - "name": "missing-attribute", - "description": "Object of class `PlanUpdateChunk` has no attribute `text`\nObject of class `TaskUpdateChunk` has no attribute `text`", - "concise_description": "Object of class `PlanUpdateChunk` has no attribute `text`\nObject of class `TaskUpdateChunk` has no attribute `text`", - "severity": "error" - }, - { - "line": 975, - "column": 23, - "stop_line": 975, - "stop_column": 27, - "path": "src/chat_sdk/thread.py", - "code": -2, - "name": "invalid-yield", - "description": "Yielded type `dict[Any, Any]` is not assignable to declared yield type `MarkdownTextChunk | PlanUpdateChunk | TaskUpdateChunk | str`", - "concise_description": "Yielded type `dict[Any, Any]` is not assignable to declared yield type `MarkdownTextChunk | PlanUpdateChunk | TaskUpdateChunk | str`", - "severity": "error" - } - ] -} \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index c563cf5..36b34b6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -80,9 +80,8 @@ dev = [ # --------------------------------------------------------------------------- # Pyrefly type checker configuration # --------------------------------------------------------------------------- -# Baseline workflow: -# Check against baseline: uv run pyrefly check --baseline=.pyrefly-baseline.json -# Refresh baseline: uv run pyrefly check --baseline=.pyrefly-baseline.json --update-baseline +# Target: zero errors. Run: +# uv run pyrefly check # --------------------------------------------------------------------------- [tool.pyrefly] @@ -99,21 +98,27 @@ python-platform = "linux" # Optional adapter deps that aren't in the default install. Treating them as # Any keeps pyrefly from flagging missing-import every time we lazy-load one. replace-imports-with-any = [ - # Slack + # Slack — covers top-level + submodule imports like slack_sdk.web.async_client "slack_sdk", - # Discord + "slack_sdk.*", + # Discord — signed-request verification uses pynacl's nacl.signing "nacl", - # Google Chat + "nacl.*", + # Google Chat — auth uses google.auth.* subpackages "google", + "google.*", # State backends "redis", + "redis.*", "asyncpg", + "asyncpg.*", # HTTP clients used in lazy paths "httpx", + "httpx.*", # GitHub App auth "jwt", + "jwt.*", ] [tool.pyrefly.errors] -# Default severity. Known issues tracked in .pyrefly-baseline.json — -# new errors fail CI, existing ones are allowed. +# CI policy: zero pyrefly errors. diff --git a/src/chat_sdk/adapters/discord/adapter.py b/src/chat_sdk/adapters/discord/adapter.py index bfbd379..3fd9b6a 100644 --- a/src/chat_sdk/adapters/discord/adapter.py +++ b/src/chat_sdk/adapters/discord/adapter.py @@ -9,12 +9,13 @@ from __future__ import annotations import hmac +import inspect import json import os import re from contextvars import ContextVar from datetime import datetime, timezone -from typing import Any +from typing import Any, Literal, cast from urllib.parse import quote from chat_sdk.adapters.discord.cards import ( @@ -52,8 +53,10 @@ FetchResult, FileUpload, FormattedContent, + LockScope, Message, MessageMetadata, + PostableRaw, RawMessage, ReactionEvent, SlashCommandEvent, @@ -160,7 +163,7 @@ def bot_user_id(self) -> str | None: return self._bot_user_id @property - def lock_scope(self) -> str | None: + def lock_scope(self) -> LockScope | None: return None @property @@ -362,7 +365,7 @@ def _handle_component_interaction( ), message_id=message_id, thread_id=thread_id, - thread=None, + thread=None, # pyrefly: ignore[bad-argument-type] # filled in by Chat adapter=self, raw=interaction, ), @@ -379,7 +382,11 @@ def _handle_application_command_interaction( self._logger.warn("Chat instance not initialized, ignoring interaction") return - data = interaction.get("data", {}) + # `interaction["data"]` is a union of several TypedDicts (one per + # interaction type). Cast to a plain dict so we can access shared + # fields like `name` and `options` without pyrefly rejecting keys + # that only appear on one variant. + data = cast("dict[str, Any]", interaction.get("data", {})) command_name = data.get("name") if not command_name: self._logger.warn("No command name in application command interaction") @@ -447,7 +454,7 @@ def _handle_application_command_interaction( is_me=user.get("id") == self._application_id, ), adapter=self, - channel=None, + channel=None, # pyrefly: ignore[bad-argument-type] # filled in by Chat raw=interaction, ) event.channel_id = channel_id # type: ignore[attr-defined] @@ -704,7 +711,7 @@ async def _handle_forwarded_reaction( self._chat.process_reaction( ReactionEvent( adapter=self, - thread=None, + thread=None, # pyrefly: ignore[bad-argument-type] # filled in by Chat thread_id=thread_id, message_id=data.get("message_id", ""), emoji=normalized, @@ -953,7 +960,7 @@ async def remove_reaction( "DELETE", ) - async def start_typing(self, thread_id: str, _status: str | None = None) -> None: + async def start_typing(self, thread_id: str, status: str | None = None) -> None: """Start typing indicator in a Discord channel or thread.""" decoded = self.decode_thread_id(thread_id) target_channel_id = decoded.thread_id or decoded.channel_id @@ -1159,7 +1166,7 @@ async def stream( accumulated += text - postable: AdapterPostableMessage = {"raw": accumulated} + postable: AdapterPostableMessage = PostableRaw(raw=accumulated) if message_id: await self.edit_message(thread_id, message_id, postable) @@ -1256,7 +1263,7 @@ def _parse_discord_message(self, raw: dict[str, Any], thread_id: str) -> Message ], ) - def _get_attachment_type(self, mime_type: str | None) -> str: + def _get_attachment_type(self, mime_type: str | None) -> Literal["audio", "file", "image", "video"]: """Determine attachment type from MIME type.""" if not mime_type: return "file" @@ -1396,23 +1403,35 @@ async def _discord_fetch( # Request/Response helpers (framework-agnostic) # ========================================================================= - async def _get_request_body(self, request: Any) -> str: + @staticmethod + async def _get_request_body(request: Any) -> str: """Extract the request body as a string.""" - if hasattr(request, "body"): - body = request.body + # `hasattr` narrows `Any` → `object` (not awaitable); using + # `getattr(..., None)` preserves `Any` for framework duck-typing. + # Handle both callable and non-callable `request.text`. Gating + # entry on callability would drop populated string attributes. + text_attr = getattr(request, "text", None) + if text_attr is not None: + if callable(text_attr): + result = text_attr() + text_attr = await result if inspect.isawaitable(result) else result + return text_attr.decode("utf-8") if isinstance(text_attr, (bytes, bytearray)) else str(text_attr) + body = getattr(request, "body", None) + if body is not None: if callable(body): body = body() + # Some frameworks expose `body` as an async method; if calling it + # produced a coroutine, await it before treating as bytes/str. + if inspect.isawaitable(body): + body = await body if hasattr(body, "read"): - raw = await body.read() if hasattr(body.read, "__await__") else body.read() - return raw.decode("utf-8") if isinstance(raw, bytes) else raw - return body.decode("utf-8") if isinstance(body, bytes) else str(body) - if hasattr(request, "text"): - if callable(request.text): - return await request.text() - return request.text - if hasattr(request, "data"): - data = request.data - return data.decode("utf-8") if isinstance(data, bytes) else str(data) + raw_result = body.read() + raw = await raw_result if inspect.isawaitable(raw_result) else raw_result + return raw.decode("utf-8") if isinstance(raw, (bytes, bytearray)) else str(raw) + return body.decode("utf-8") if isinstance(body, (bytes, bytearray)) else str(body) + data = getattr(request, "data", None) + if data is not None: + return data.decode("utf-8") if isinstance(data, (bytes, bytearray)) else str(data) return "" def _get_header(self, request: Any, name: str) -> str | None: diff --git a/src/chat_sdk/adapters/discord/cards.py b/src/chat_sdk/adapters/discord/cards.py index 223edef..c5114b3 100644 --- a/src/chat_sdk/adapters/discord/cards.py +++ b/src/chat_sdk/adapters/discord/cards.py @@ -7,7 +7,7 @@ from __future__ import annotations -from typing import Any +from typing import Any, cast from chat_sdk.adapters.discord.types import DiscordActionRow, DiscordButton from chat_sdk.cards import ( @@ -113,12 +113,12 @@ def _process_child( elif child_type == "fields": _convert_fields_element(child, fields) # type: ignore[arg-type] elif child_type == "link": - label = child.get("label", "") # type: ignore[union-attr] - url = child.get("url", "") # type: ignore[union-attr] + label = cast("str", child.get("label", "")) + url = cast("str", child.get("url", "")) text_parts.append(f"[{_convert_emoji(label)}]({url})") elif child_type == "table": - headers = child.get("headers", []) # type: ignore[union-attr] - rows = child.get("rows", []) # type: ignore[union-attr] + headers = cast("list[str]", child.get("headers", [])) + rows = cast("list[list[str]]", child.get("rows", [])) text_parts.append("\n".join(render_gfm_table(headers, rows))) else: text = card_child_to_fallback_text(child) @@ -270,8 +270,8 @@ def _child_to_fallback_text(child: CardChild) -> str | None: if (t := _child_to_fallback_text(c)) ) if child_type == "table": - headers = child.get("headers", []) # type: ignore[union-attr] - rows = child.get("rows", []) # type: ignore[union-attr] + headers = cast("list[str]", child.get("headers", [])) + rows = cast("list[list[str]]", child.get("rows", [])) return f"```\n{table_element_to_ascii(headers, rows)}\n```" if child_type == "divider": return "---" diff --git a/src/chat_sdk/adapters/github/adapter.py b/src/chat_sdk/adapters/github/adapter.py index a39bac2..1f11849 100644 --- a/src/chat_sdk/adapters/github/adapter.py +++ b/src/chat_sdk/adapters/github/adapter.py @@ -11,13 +11,14 @@ import asyncio import hashlib import hmac +import inspect import json import os import re import time from collections.abc import AsyncIterable from datetime import datetime, timezone -from typing import Any, Literal +from typing import Any, Literal, cast from chat_sdk.adapters.github.cards import card_to_github_markdown from chat_sdk.adapters.github.format_converter import GitHubFormatConverter @@ -26,11 +27,13 @@ GitHubIssueComment, GitHubRawMessage, GitHubReactionContent, + GitHubRepository, GitHubReviewComment, GitHubThreadId, GitHubUser, IssueCommentWebhookPayload, PullRequestReviewCommentWebhookPayload, + _GitHubAdapterConfigInternal, ) from chat_sdk.emoji import convert_emoji_placeholders from chat_sdk.logger import ConsoleLogger, Logger @@ -47,8 +50,10 @@ FormattedContent, ListThreadsOptions, ListThreadsResult, + LockScope, Message, MessageMetadata, + PostableMarkdown, RawMessage, StreamChunk, StreamOptions, @@ -89,7 +94,10 @@ class GitHubAdapter: """ def __init__(self, config: GitHubAdapterConfig | None = None) -> None: - config = config or {} + # The public API surface is the auth-mode-specific TypedDict union + # above; internally we duck-type with `.get()` so narrow to the + # superset TypedDict that admits every possible key. + config = cast(_GitHubAdapterConfigInternal, config or {}) self._name = "github" webhook_secret = config.get("webhook_secret") or os.environ.get("GITHUB_WEBHOOK_SECRET") @@ -177,7 +185,7 @@ def bot_user_id(self) -> str | None: return str(self._bot_user_id) if self._bot_user_id else None @property - def lock_scope(self) -> str | None: + def lock_scope(self) -> LockScope | None: return None @property @@ -360,7 +368,7 @@ def _handle_review_comment( def _parse_issue_comment( self, comment: GitHubIssueComment, - repository: dict[str, Any], + repository: GitHubRepository, pr_number: int, thread_id: str, thread_type: Literal["pr", "issue"] = "pr", @@ -404,7 +412,7 @@ def _parse_issue_comment( def _parse_review_comment( self, comment: GitHubReviewComment, - repository: dict[str, Any], + repository: GitHubRepository, pr_number: int, thread_id: str, ) -> Message: @@ -523,7 +531,7 @@ async def stream( text += chunk elif hasattr(chunk, "type") and chunk.type == "markdown_text": text += chunk.text - return await self.post_message(thread_id, {"markdown": text}) + return await self.post_message(thread_id, PostableMarkdown(markdown=text)) @staticmethod def _build_raw_message(decoded: Any, comment: dict[str, Any]) -> dict[str, Any]: @@ -884,8 +892,11 @@ async def fetch_channel_info(self, channel_id: str) -> ChannelInfo: def parse_message(self, raw: GitHubRawMessage) -> Message: """Parse a raw message into normalized format.""" + # `raw` is a GitHubRawMessage dict with keys of varying value-types + # depending on the event variant; `.get()` unions to `object | str`. + # Narrow via casts where we've runtime-verified via `raw.get("type")`. if raw.get("type") == "issue_comment": - thread_type = raw.get("thread_type", "pr") or "pr" + thread_type = cast("Literal['issue', 'pr']", raw.get("thread_type", "pr") or "pr") thread_id = self.encode_thread_id( GitHubThreadId( owner=raw["repository"]["owner"]["login"], @@ -895,10 +906,17 @@ def parse_message(self, raw: GitHubRawMessage) -> Message: ) ) return self._parse_issue_comment( - raw["comment"], raw["repository"], raw["pr_number"], thread_id, thread_type + cast("GitHubIssueComment", raw["comment"]), + raw["repository"], + raw["pr_number"], + thread_id, + thread_type, ) else: - root_comment_id = raw["comment"].get("in_reply_to_id") or raw["comment"]["id"] + root_comment_id = cast( + "int | None", + raw["comment"].get("in_reply_to_id") or raw["comment"]["id"], + ) thread_id = self.encode_thread_id( GitHubThreadId( owner=raw["repository"]["owner"]["login"], @@ -907,7 +925,12 @@ def parse_message(self, raw: GitHubRawMessage) -> Message: review_comment_id=root_comment_id, ) ) - return self._parse_review_comment(raw["comment"], raw["repository"], raw["pr_number"], thread_id) + return self._parse_review_comment( + cast("GitHubReviewComment", raw["comment"]), + raw["repository"], + raw["pr_number"], + thread_id, + ) def render_formatted(self, content: FormattedContent) -> str: """Render formatted content to GitHub markdown.""" @@ -1064,11 +1087,26 @@ async def _get_installation_id(self, owner: str, repo: str) -> int | None: @staticmethod async def _get_request_body(request: Any) -> str: """Extract body text from a request object.""" - if hasattr(request, "text") and callable(request.text): - return await request.text() - if hasattr(request, "body"): - body = request.body - return body.decode("utf-8") if isinstance(body, bytes) else str(body) + # `hasattr` narrows `Any` → `object` (which is not awaitable), so + # `getattr(..., None)` keeps `Any` for the framework duck-type path. + # Handle both callable (`async def text(self)`) and non-callable + # (`text: str` property) cases — gating entry on callability + # would silently drop valid string attributes. + text_attr = getattr(request, "text", None) + if text_attr is not None: + if callable(text_attr): + result = text_attr() + text_attr = await result if inspect.isawaitable(result) else result + return text_attr.decode("utf-8") if isinstance(text_attr, (bytes, bytearray)) else str(text_attr) + body = getattr(request, "body", None) + if body is not None: + # Some frameworks expose `body` as an async method; if calling it + # produced a coroutine, await it before treating as bytes/str. + if callable(body): + body = body() + if inspect.isawaitable(body): + body = await body + return body.decode("utf-8") if isinstance(body, (bytes, bytearray)) else str(body) return "" @staticmethod diff --git a/src/chat_sdk/adapters/github/cards.py b/src/chat_sdk/adapters/github/cards.py index 33c2df2..3790864 100644 --- a/src/chat_sdk/adapters/github/cards.py +++ b/src/chat_sdk/adapters/github/cards.py @@ -6,7 +6,7 @@ from __future__ import annotations -from typing import Any +from typing import Any, cast from chat_sdk.cards import ( CardChild, @@ -92,42 +92,50 @@ def card_to_github_markdown(card: CardElement) -> str: def _render_child(child: CardChild) -> list[str]: - """Render a card child element to markdown lines.""" + """Render a card child element to markdown lines. + + The per-type helpers below accept `dict[str, Any]` because they access + dynamic keys (`content`, `label`, etc.) that are specific to one + variant of the `CardChild` union. The narrowing happens via the + `child_type` check, so casting to `dict[str, Any]` at the call sites + is safe and keeps the helpers simple. + """ child_type = child.get("type", "") if child_type == "text": - return _render_text(child) + return _render_text(cast("dict[str, Any]", child)) if child_type == "fields": - return _render_fields(child) + return _render_fields(cast("dict[str, Any]", child)) if child_type == "actions": - return _render_actions(child) + return _render_actions(cast("dict[str, Any]", child)) if child_type == "section": # Flatten section children result: list[str] = [] - for section_child in child.get("children", []): + section_children = cast("list[CardChild]", child.get("children", [])) + for section_child in section_children: result.extend(_render_child(section_child)) return result if child_type == "image": - alt = child.get("alt", "") - url = child.get("url", "") + alt = cast("str", child.get("alt", "")) + url = cast("str", child.get("url", "")) if alt: return [f"![{_escape_markdown(alt)}]({url})"] return [f"![]({url})"] if child_type == "link": - label = child.get("label", "") - url = child.get("url", "") + label = cast("str", child.get("label", "")) + url = cast("str", child.get("url", "")) return [f"[{_escape_markdown(label)}]({url})"] if child_type == "divider": return ["---"] if child_type == "table": - return _render_table(child) + return _render_table(cast("dict[str, Any]", child)) # Fallback text = card_child_to_fallback_text(child) @@ -233,7 +241,7 @@ def _child_to_plain_text(child: CardChild) -> str | None: return None if child_type == "table": - return "\n".join(_render_table(child)) + return "\n".join(_render_table(cast("dict[str, Any]", child))) if child_type == "section": return "\n".join( diff --git a/src/chat_sdk/adapters/github/types.py b/src/chat_sdk/adapters/github/types.py index 0c24979..f88882e 100644 --- a/src/chat_sdk/adapters/github/types.py +++ b/src/chat_sdk/adapters/github/types.py @@ -80,7 +80,23 @@ class GitHubAdapterAutoConfig(GitHubAdapterBaseConfig, total=False): """Configuration with no auth fields - will auto-detect from env vars.""" -# Union of all configuration types +class _GitHubAdapterConfigInternal(GitHubAdapterBaseConfig, total=False): + """Internal superset used for duck-typed `config.get(...)` auth detection. + + The public API surface is the auth-mode-specific TypedDicts above — they + document which fields are valid for each auth mode. Inside `__init__` we + do mode detection via `.get("token")`, `.get("app_id")`, etc., which + requires a type that admits every possible key. This type is never + exposed to consumers. + """ + + token: str + app_id: str + installation_id: int + private_key: str + + +# Union of all public configuration types GitHubAdapterConfig = ( GitHubAdapterPATConfig | GitHubAdapterAppConfig | GitHubAdapterMultiTenantAppConfig | GitHubAdapterAutoConfig ) diff --git a/src/chat_sdk/adapters/google_chat/adapter.py b/src/chat_sdk/adapters/google_chat/adapter.py index 97c6340..143f68e 100644 --- a/src/chat_sdk/adapters/google_chat/adapter.py +++ b/src/chat_sdk/adapters/google_chat/adapter.py @@ -11,13 +11,14 @@ import asyncio import contextlib +import inspect import json import os import re import time from collections.abc import AsyncIterable, Awaitable, Callable from datetime import datetime, timezone -from typing import Any +from typing import Any, NoReturn from chat_sdk.adapters.google_chat.cards import card_to_google_card from chat_sdk.adapters.google_chat.format_converter import GoogleChatFormatConverter @@ -69,7 +70,9 @@ FormattedContent, ListThreadsOptions, ListThreadsResult, + LockScope, Message, + PostableMarkdown, RawMessage, ReactionEvent, StateAdapter, @@ -130,7 +133,7 @@ def __init__(self, config: GoogleChatAdapterConfig | None = None) -> None: config = GoogleChatAdapterConfig() self._name = "gchat" - self._lock_scope: str | None = None + self._lock_scope: LockScope | None = None self._persist_message_history: bool | None = None self._logger: Logger = config.logger or ConsoleLogger("info").child("gchat") self._user_name = config.user_name or "bot" @@ -211,7 +214,7 @@ def bot_user_id(self) -> str | None: return self._bot_user_id @property - def lock_scope(self) -> str | None: + def lock_scope(self) -> LockScope | None: return self._lock_scope @property @@ -758,17 +761,35 @@ async def handle_webhook( except Exception: pass - # Parse request body + # Parse request body. `hasattr` narrows `Any` → `object` (not + # awaitable); `getattr(..., None)` preserves `Any` for the + # framework duck-typed path. + # `request.text` may be either an async method (aiohttp, FastAPI) + # or a populated string/bytes attribute (Django raw HttpRequest, + # some mock objects). Handle both — testing callability inside + # the non-None branch, not gating entry on it, so non-callable + # text attributes aren't silently dropped to the body branch. body: str - if hasattr(request, "text") and callable(request.text): - body = await request.text() - elif hasattr(request, "body"): - raw_body = request.body - body = raw_body.decode("utf-8") if isinstance(raw_body, bytes) else str(raw_body) - elif isinstance(request, dict): - body = json.dumps(request) + text_attr = getattr(request, "text", None) + if text_attr is not None: + if callable(text_attr): + result = text_attr() + text_attr = await result if inspect.isawaitable(result) else result + body = text_attr.decode("utf-8") if isinstance(text_attr, (bytes, bytearray)) else str(text_attr) else: - body = str(request) + raw_body = getattr(request, "body", None) + if raw_body is not None: + # Some frameworks expose `body` as an async method; call and + # await if needed before treating as bytes/str. + if callable(raw_body): + raw_body = raw_body() + if inspect.isawaitable(raw_body): + raw_body = await raw_body + body = raw_body.decode("utf-8") if isinstance(raw_body, (bytes, bytearray)) else str(raw_body) + elif isinstance(request, dict): + body = json.dumps(request) + else: + body = str(request) self._logger.debug("GChat webhook raw body", {"body": body}) @@ -1022,7 +1043,7 @@ async def build_reaction_event() -> ReactionEvent: reaction_user = reaction.get("user") or {} return ReactionEvent( adapter=self, - thread=None, + thread=None, # pyrefly: ignore[bad-argument-type] # filled in by Chat thread_id=thread_id, message_id=message_name, user=Author( @@ -1190,7 +1211,7 @@ def _handle_card_click( action_event = ActionEvent( adapter=self, - thread=None, + thread=None, # pyrefly: ignore[bad-argument-type] # filled in by Chat thread_id=thread_id, message_id=(message or {}).get("name", ""), user=Author( @@ -1594,7 +1615,7 @@ async def stream( accumulated += chunk elif hasattr(chunk, "type") and chunk.type == "markdown_text": accumulated += chunk.text - return await self.post_message(thread_id, {"markdown": accumulated}) + return await self.post_message(thread_id, PostableMarkdown(markdown=accumulated)) # ========================================================================= # Reactions @@ -2644,11 +2665,12 @@ async def _fetch_data() -> bytes: # Error handling # ========================================================================= - def _handle_google_chat_error(self, error: Any, context: str | None = None) -> Any: + def _handle_google_chat_error(self, error: Any, context: str | None = None) -> NoReturn: """Handle Google Chat API errors with proper error classification. - Always re-raises. Returns Never (but typed as Any so callers satisfy - return type without explicit annotation). + Always re-raises — the `NoReturn` annotation lets type checkers skip + the "missing return" warning for callers that rely on this to + propagate out of a `try/except` block. """ error_code = getattr(error, "code", None) error_message = getattr(error, "message", str(error)) diff --git a/src/chat_sdk/adapters/google_chat/cards.py b/src/chat_sdk/adapters/google_chat/cards.py index 36e1070..c8205cf 100644 --- a/src/chat_sdk/adapters/google_chat/cards.py +++ b/src/chat_sdk/adapters/google_chat/cards.py @@ -8,7 +8,7 @@ from __future__ import annotations -from typing import Any +from typing import Any, cast from chat_sdk.cards import ( CardChild, @@ -73,7 +73,7 @@ def card_to_google_card( sections.append({"widgets": current_widgets}) current_widgets = [] # Convert section as its own section - section_widgets = _convert_section_to_widgets(child, opts.get("endpoint_url")) + section_widgets = _convert_section_to_widgets(cast("dict[str, Any]", child), opts.get("endpoint_url")) sections.append({"widgets": section_widgets}) else: # Add to current widgets @@ -108,24 +108,30 @@ def _convert_child_to_widgets( child: CardChild, endpoint_url: str | None = None, ) -> list[dict[str, Any]]: - """Convert a card child element to Google Chat widgets.""" + """Convert a card child element to Google Chat widgets. + + The per-type helpers below accept `dict[str, Any]` because they reach + for variant-specific keys (`content`, `label`, `url`, etc.). The + `child_type` check narrows the union, so the `cast` is safe and lets + the helpers stay simple. + """ child_type = child.get("type") if child_type == "text": - return [_convert_text_to_widget(child)] + return [_convert_text_to_widget(cast("dict[str, Any]", child))] elif child_type == "image": - return [_convert_image_to_widget(child)] + return [_convert_image_to_widget(cast("dict[str, Any]", child))] elif child_type == "divider": return [_convert_divider_to_widget()] elif child_type == "actions": - return [_convert_actions_to_widget(child, endpoint_url)] + return [_convert_actions_to_widget(cast("dict[str, Any]", child), endpoint_url)] elif child_type == "section": - return _convert_section_to_widgets(child, endpoint_url) + return _convert_section_to_widgets(cast("dict[str, Any]", child), endpoint_url) elif child_type == "fields": - return _convert_fields_to_widgets(child) + return _convert_fields_to_widgets(cast("dict[str, Any]", child)) elif child_type == "link": - label = child.get("label", "") - url = child.get("url", "") + label = cast("str", child.get("label", "")) + url = cast("str", child.get("url", "")) return [ { "textParagraph": { @@ -134,7 +140,7 @@ def _convert_child_to_widgets( }, ] elif child_type == "table": - return [_convert_table_to_widget(child)] + return [_convert_table_to_widget(cast("dict[str, Any]", child))] else: text = card_child_to_fallback_text(child) if text: diff --git a/src/chat_sdk/adapters/google_chat/workspace_events.py b/src/chat_sdk/adapters/google_chat/workspace_events.py index a216031..5d29c7a 100644 --- a/src/chat_sdk/adapters/google_chat/workspace_events.py +++ b/src/chat_sdk/adapters/google_chat/workspace_events.py @@ -17,6 +17,7 @@ import asyncio import base64 +import inspect import json from dataclasses import dataclass from typing import Any @@ -340,10 +341,12 @@ async def _get_access_token( elif isinstance(auth, WorkspaceEventsAuthADC): return await _get_adc_token(scopes, http_session) elif isinstance(auth, dict) and "auth" in auth: - # Custom auth - assume it provides a token directly + # Custom auth — `auth["auth"]` can be sync or async. `isawaitable` + # narrows the result so sync callables work without a TypeError. custom_auth = auth["auth"] if callable(custom_auth): - return await custom_auth() + result = custom_auth() + return str(await result if inspect.isawaitable(result) else result) return str(custom_auth) else: raise ValueError("Unsupported auth type for workspace events") diff --git a/src/chat_sdk/adapters/linear/adapter.py b/src/chat_sdk/adapters/linear/adapter.py index 5dc2b79..ea724f6 100644 --- a/src/chat_sdk/adapters/linear/adapter.py +++ b/src/chat_sdk/adapters/linear/adapter.py @@ -11,12 +11,13 @@ import asyncio import hashlib import hmac +import inspect import json import os import re import time from datetime import datetime, timezone -from typing import Any +from typing import Any, cast from chat_sdk.adapters.linear.cards import card_to_linear_markdown from chat_sdk.adapters.linear.format_converter import LinearFormatConverter @@ -47,8 +48,10 @@ FetchOptions, FetchResult, FormattedContent, + LockScope, Message, MessageMetadata, + PostableRaw, RawMessage, StreamOptions, ThreadInfo, @@ -169,7 +172,7 @@ def bot_user_id(self) -> str | None: return self._bot_user_id @property - def lock_scope(self) -> str | None: + def lock_scope(self) -> LockScope | None: return None @property @@ -298,13 +301,15 @@ async def handle_webhook( ) return self._make_response("Webhook expired", 401) - # Handle events based on type + # Handle events based on type. The payload shape is determined by + # `type` at runtime — cast to the matching TypedDict so each handler + # sees the right variant. payload_type = payload.get("type") if payload_type == "Comment": if payload.get("action") == "create": - self._handle_comment_created(payload, options) + self._handle_comment_created(cast("CommentWebhookPayload", payload), options) elif payload_type == "Reaction": - self._handle_reaction(payload) + self._handle_reaction(cast("ReactionWebhookPayload", payload)) return self._make_response("ok", 200) @@ -337,18 +342,23 @@ def _handle_comment_created( self._logger.warn("Chat instance not initialized, ignoring comment") return - data = payload.get("data", {}) - actor = payload.get("actor", {}) + # TypedDict `.get()` unions every field-type from the union of shapes + # (comment-created payloads vs older camel/snake fallbacks), producing + # `object | str`. Cast to `str` where we've runtime-narrowed via the + # truthy check — the dispatch block already filtered to `Comment` + # events, so these keys are known to be strings. + data = cast("LinearCommentData", payload.get("data", {})) + actor = cast("LinearWebhookActor", payload.get("actor", {})) # Skip non-issue comments - issue_id = data.get("issueId") or data.get("issue_id") + issue_id = cast("str | None", data.get("issueId") or data.get("issue_id")) if not issue_id: self._logger.debug("Ignoring non-issue comment", {"commentId": data.get("id")}) return # Determine thread parent_id = data.get("parentId") or data.get("parent_id") - root_comment_id = parent_id or data.get("id") + root_comment_id = cast("str | None", parent_id or data.get("id")) thread_id = self.encode_thread_id( LinearThreadId( issue_id=issue_id, @@ -392,8 +402,12 @@ def _build_message( thread_id: str, ) -> Message: """Build a Message from a Linear comment and actor.""" - text = comment.get("body", "") - user_id = comment.get("userId") or comment.get("user_id", "") + # `comment.get("body")` unions every value type across the TypedDict + # variants, giving `object | str`. Cast to `str` where the runtime + # shape guarantees a string (Linear webhook `Comment` payloads + # always have `body`, `userId`, `createdAt`, `updatedAt` as strings). + text = cast("str", comment.get("body", "")) + user_id = cast("str", comment.get("userId") or comment.get("user_id", "")) author = Author( user_id=user_id, @@ -405,8 +419,8 @@ def _build_message( formatted = self._format_converter.to_ast(text) - created_at = comment.get("createdAt") or comment.get("created_at", "") - updated_at = comment.get("updatedAt") or comment.get("updated_at", "") + created_at = cast("str", comment.get("createdAt") or comment.get("created_at", "")) + updated_at = cast("str", comment.get("updatedAt") or comment.get("updated_at", "")) return Message( id=comment.get("id", ""), @@ -537,7 +551,7 @@ async def edit_message( ), ) - async def delete_message(self, _thread_id: str, message_id: str) -> None: + async def delete_message(self, thread_id: str, message_id: str) -> None: """Delete a message (delete a comment).""" await self._ensure_valid_token() @@ -554,7 +568,7 @@ async def delete_message(self, _thread_id: str, message_id: str) -> None: async def add_reaction( self, - _thread_id: str, + thread_id: str, message_id: str, emoji: EmojiValue | str, ) -> None: @@ -575,14 +589,14 @@ async def add_reaction( async def remove_reaction( self, - _thread_id: str, - _message_id: str, - _emoji: EmojiValue | str, + thread_id: str, + message_id: str, + emoji: EmojiValue | str, ) -> None: """Remove a reaction from a comment (limited support).""" self._logger.warn("removeReaction is not fully supported on Linear - reaction ID lookup would be required") - async def start_typing(self, _thread_id: str, _status: str | None = None) -> None: + async def start_typing(self, thread_id: str, status: str | None = None) -> None: """Start typing indicator. Not supported by Linear.""" pass @@ -739,7 +753,7 @@ def _comment_node_to_message( "userId": user_id, "createdAt": node.get("createdAt", ""), "updatedAt": node.get("updatedAt", ""), - "url": node.get("url"), + "url": node.get("url", ""), }, ), author=Author( @@ -832,13 +846,19 @@ def channel_id_from_thread_id(self, thread_id: str) -> str: return f"linear:{decoded.issue_id}" def parse_message(self, raw: LinearRawMessage) -> Message: - """Parse platform message format to normalized format.""" + """Parse platform message format to normalized format. + + TypedDict `.get()` unions every value-type across camel/snake-case + aliases, producing `object | str`. Cast the string fields we know + are strings at runtime so downstream constructors (`Author`, + `_parse_iso`) receive `str` instead of `object`. + """ comment = raw.get("comment", {}) - text = comment.get("body", "") - user_id = comment.get("userId") or comment.get("user_id", "") + text = cast("str", comment.get("body", "")) + user_id = cast("str", comment.get("userId") or comment.get("user_id", "")) - created_at = comment.get("createdAt") or comment.get("created_at", "") - updated_at = comment.get("updatedAt") or comment.get("updated_at", "") + created_at = cast("str", comment.get("createdAt") or comment.get("created_at", "")) + updated_at = cast("str", comment.get("updatedAt") or comment.get("updated_at", "")) return Message( id=comment.get("id", ""), @@ -891,7 +911,7 @@ async def stream( # Post the accumulated text as a single comment if accumulated: - postable: AdapterPostableMessage = {"raw": accumulated} + postable: AdapterPostableMessage = PostableRaw(raw=accumulated) result = await self.post_message(thread_id, postable) message_id = result.id @@ -958,23 +978,35 @@ async def _graphql_query( # Request/Response helpers (framework-agnostic) # ========================================================================= - async def _get_request_body(self, request: Any) -> str: + @staticmethod + async def _get_request_body(request: Any) -> str: """Extract the request body as a string.""" - if hasattr(request, "body"): - body = request.body + # `hasattr` narrows `Any` → `object` (not awaitable); using + # `getattr(..., None)` preserves `Any` for framework duck-typing. + # Handle both callable and non-callable `request.text`. Gating + # entry on callability would drop populated string attributes. + text_attr = getattr(request, "text", None) + if text_attr is not None: + if callable(text_attr): + result = text_attr() + text_attr = await result if inspect.isawaitable(result) else result + return text_attr.decode("utf-8") if isinstance(text_attr, (bytes, bytearray)) else str(text_attr) + body = getattr(request, "body", None) + if body is not None: if callable(body): body = body() + # Some frameworks expose `body` as an async method; if calling it + # produced a coroutine, await it before treating as bytes/str. + if inspect.isawaitable(body): + body = await body if hasattr(body, "read"): - raw = await body.read() if hasattr(body.read, "__await__") else body.read() - return raw.decode("utf-8") if isinstance(raw, bytes) else raw - return body.decode("utf-8") if isinstance(body, bytes) else str(body) - if hasattr(request, "text"): - if callable(request.text): - return await request.text() - return request.text - if hasattr(request, "data"): - data = request.data - return data.decode("utf-8") if isinstance(data, bytes) else str(data) + raw_result = body.read() + raw = await raw_result if inspect.isawaitable(raw_result) else raw_result + return raw.decode("utf-8") if isinstance(raw, (bytes, bytearray)) else str(raw) + return body.decode("utf-8") if isinstance(body, (bytes, bytearray)) else str(body) + data = getattr(request, "data", None) + if data is not None: + return data.decode("utf-8") if isinstance(data, (bytes, bytearray)) else str(data) return "" def _get_header(self, request: Any, name: str) -> str | None: diff --git a/src/chat_sdk/adapters/linear/cards.py b/src/chat_sdk/adapters/linear/cards.py index 7f7d938..0860363 100644 --- a/src/chat_sdk/adapters/linear/cards.py +++ b/src/chat_sdk/adapters/linear/cards.py @@ -9,6 +9,8 @@ from __future__ import annotations +from typing import cast + from chat_sdk.cards import ( ActionsElement, CardChild, @@ -88,14 +90,14 @@ def _render_child(child: CardChild) -> list[str]: result.extend(_render_child(c)) return result if child_type == "image": - alt = child.get("alt", "") # type: ignore[union-attr] - url = child.get("url", "") # type: ignore[union-attr] + alt = cast("str", child.get("alt", "")) + url = cast("str", child.get("url", "")) if alt: return [f"![{_escape_markdown(alt)}]({url})"] return [f"![]({url})"] if child_type == "link": - label = child.get("label", "") # type: ignore[union-attr] - url = child.get("url", "") # type: ignore[union-attr] + label = cast("str", child.get("label", "")) + url = cast("str", child.get("url", "")) return [f"[{_escape_markdown(label)}]({url})"] if child_type == "divider": return ["---"] @@ -185,8 +187,8 @@ def _child_to_plain_text(child: CardChild) -> str | None: # Actions are interactive-only -- exclude from fallback text. return None if child_type == "table": - headers = child.get("headers", []) # type: ignore[union-attr] - rows = child.get("rows", []) # type: ignore[union-attr] + headers = cast("list[str]", child.get("headers", [])) + rows = cast("list[list[str]]", child.get("rows", [])) return "\n".join(render_gfm_table(headers, rows)) if child_type == "section": parts = [ diff --git a/src/chat_sdk/adapters/slack/adapter.py b/src/chat_sdk/adapters/slack/adapter.py index d1f70e6..85d12e2 100644 --- a/src/chat_sdk/adapters/slack/adapter.py +++ b/src/chat_sdk/adapters/slack/adapter.py @@ -13,6 +13,7 @@ import contextvars import hashlib import hmac +import inspect import json import os import re @@ -21,7 +22,7 @@ from collections.abc import AsyncIterable, Awaitable, Callable from contextvars import ContextVar from datetime import datetime, timezone -from typing import Any +from typing import Any, NoReturn, cast from urllib.parse import parse_qs from chat_sdk.adapters.slack.cards import ( @@ -51,6 +52,7 @@ ) from chat_sdk.emoji import convert_emoji_placeholders, emoji_to_slack, resolve_emoji_from_slack from chat_sdk.logger import ConsoleLogger, Logger +from chat_sdk.modals import ModalElement from chat_sdk.shared.adapter_utils import extract_card, extract_files from chat_sdk.shared.errors import AdapterRateLimitError, AuthenticationError, ValidationError from chat_sdk.types import ( @@ -73,6 +75,7 @@ LinkPreview, ListThreadsOptions, ListThreadsResult, + LockScope, MemberJoinedChannelEvent, Message, MessageMetadata, @@ -203,7 +206,7 @@ def __init__(self, config: SlackAdapterConfig | None = None) -> None: self._bot_id: str | None = None # Bot app ID (B_xxx) self._chat: ChatInstance | None = None self._format_converter = SlackFormatConverter() - self._lock_scope = "thread" + self._lock_scope: LockScope = "thread" self._persist_message_history = False # Channel external/shared cache @@ -245,7 +248,7 @@ def bot_user_id(self) -> str | None: return self._bot_user_id @property - def lock_scope(self) -> str: + def lock_scope(self) -> LockScope: return self._lock_scope @property @@ -433,6 +436,10 @@ async def get_installation(self, team_id: str) -> SlackInstallation | None: bot_user_id = (stored.get("botUserId") or stored.get("bot_user_id") or "") if isinstance(stored, dict) else "" team_name = (stored.get("teamName") or stored.get("team_name") or "") if isinstance(stored, dict) else "" if self._encryption_key and is_encrypted_token_data(bot_token_raw): + # `is_encrypted_token_data` is a runtime type guard but doesn't + # carry TypeGuard narrowing, so pyrefly still sees `None`. Assert + # to collapse the Optional for the field access below. + assert bot_token_raw is not None decrypted = decrypt_token( EncryptedTokenData( iv=bot_token_raw["iv"], @@ -678,16 +685,33 @@ async def handle_webhook(self, request: Any, options: WebhookOptions | None = No Returns a dict with ``body`` and ``status`` keys. """ - # Read the raw body - if hasattr(request, "text") and callable(request.text): - body: str = await request.text() - elif hasattr(request, "body"): - raw = request.body - if asyncio.iscoroutine(raw) or asyncio.isfuture(raw): - raw = await raw - body = raw.decode("utf-8") if isinstance(raw, (bytes, bytearray)) else str(raw) + # Read the raw body. `hasattr` narrows `Any` → `object` (not + # awaitable), so we use `getattr(..., None)` to preserve the + # `Any` type across the duck-typed framework branches. + # Handle both callable (`async def text(self)`) and non-callable + # (`text: str` attribute) forms of `request.text`. Gating entry + # on callability would drop populated string attributes. + text_attr = getattr(request, "text", None) + body: str + if text_attr is not None: + if callable(text_attr): + result = text_attr() + text_attr = await result if inspect.isawaitable(result) else result + body = text_attr.decode("utf-8") if isinstance(text_attr, (bytes, bytearray)) else str(text_attr) else: - body = str(request) + raw = getattr(request, "body", None) + if raw is not None: + # Some frameworks expose `body` as an async method (e.g. + # `async def body(self)`) — call it, then await if the + # result is awaitable. Previously we only handled the + # coroutine-as-attribute case, not the async-method case. + if callable(raw): + raw = raw() + if asyncio.iscoroutine(raw) or asyncio.isfuture(raw) or inspect.isawaitable(raw): + raw = await raw + body = raw.decode("utf-8") if isinstance(raw, (bytes, bytearray)) else str(raw) + else: + body = str(request) self._logger.debug("Slack webhook raw body", {"body": body[:500]}) @@ -700,7 +724,7 @@ async def handle_webhook(self, request: Any, options: WebhookOptions | None = No return {"body": "Invalid signature", "status": 401} # Form-urlencoded payloads (interactive + slash commands) - content_type = headers.get("content-type", headers.get("Content-Type", "")) + content_type = headers.get("content-type") or headers.get("Content-Type") or "" if "application/x-www-form-urlencoded" in content_type: params = parse_qs(body, keep_blank_values=True) @@ -900,7 +924,7 @@ async def _handle_slash_command( is_me=False, ), adapter=self, - channel=None, # chat.py's _handle_slash_command_event creates the ChannelImpl + channel=None, # pyrefly: ignore[bad-argument-type] # filled in by Chat raw={k: v[0] for k, v in params.items()} if params else {}, trigger_id=trigger_id, ) @@ -959,7 +983,7 @@ def _handle_block_actions(self, payload: dict[str, Any], options: WebhookOptions ), message_id=message_id, thread_id=thread_id, - thread=None, + thread=None, # pyrefly: ignore[bad-argument-type] # filled in by Chat adapter=self, raw=payload, trigger_id=payload.get("trigger_id"), @@ -1070,7 +1094,7 @@ def _modal_response_to_slack(self, response: ModalResponse, context_id: str | No private_metadata=modal.get("private_metadata"), ) ) - view = modal_to_slack_view(modal, metadata) + view = modal_to_slack_view(cast(ModalElement, modal), metadata) return {"response_action": response.action, "view": view} return {} @@ -1178,7 +1202,7 @@ async def _resolve_and_process() -> None: ), message_id=message_id, thread_id=thread_id, - thread=None, + thread=None, # pyrefly: ignore[bad-argument-type] # filled in by Chat raw=event, adapter=self, ) @@ -2329,7 +2353,7 @@ async def open_modal(self, trigger_id: str, modal: dict[str, Any], context_id: s private_metadata=modal.get("private_metadata"), ) ) - view = modal_to_slack_view(modal, metadata) + view = modal_to_slack_view(cast(ModalElement, modal), metadata) self._logger.debug( "Slack API: views.open", @@ -2346,7 +2370,7 @@ async def open_modal(self, trigger_id: str, modal: dict[str, Any], context_id: s async def update_modal(self, view_id: str, modal: dict[str, Any]) -> dict[str, str]: """Update an existing modal using views.update.""" - view = modal_to_slack_view(modal) + view = modal_to_slack_view(cast(ModalElement, modal)) try: client = self._get_client() @@ -2751,10 +2775,12 @@ def render_formatted(self, content: FormattedContent) -> str: # Error handling # ================================================================== - def _handle_slack_error(self, error: Any) -> None: + def _handle_slack_error(self, error: Any) -> NoReturn: """Re-raise Slack errors with appropriate SDK error types. - Never returns (always raises). + Always raises — the `NoReturn` annotation lets type checkers skip + the "missing return" warning for callers that rely on this to + propagate out of a `try/except` block. """ # slack_sdk's SlackApiError has a .response attribute (SlackResponse) # SlackResponse has a .data dict and an .get() method diff --git a/src/chat_sdk/adapters/teams/adapter.py b/src/chat_sdk/adapters/teams/adapter.py index 562ac06..9439709 100644 --- a/src/chat_sdk/adapters/teams/adapter.py +++ b/src/chat_sdk/adapters/teams/adapter.py @@ -10,11 +10,12 @@ import asyncio import base64 +import inspect import json import os import re from datetime import datetime, timezone -from typing import Any, NoReturn +from typing import Any, Literal, NoReturn from chat_sdk.adapters.teams.cards import card_to_adaptive_card from chat_sdk.adapters.teams.format_converter import TeamsFormatConverter @@ -45,6 +46,7 @@ FetchOptions, FetchResult, FormattedContent, + LockScope, Message, MessageMetadata, RawMessage, @@ -195,7 +197,7 @@ def bot_user_id(self) -> str | None: return self._bot_user_id @property - def lock_scope(self) -> str | None: + def lock_scope(self) -> LockScope | None: return None @property @@ -413,7 +415,7 @@ def _handle_message_action( ), message_id=activity.get("replyToId") or activity.get("id", ""), thread_id=thread_id, - thread=None, + thread=None, # pyrefly: ignore[bad-argument-type] # filled in by Chat adapter=self, raw=activity, ), @@ -456,7 +458,7 @@ async def _handle_adaptive_card_action( ), message_id=activity.get("replyToId") or activity.get("id", ""), thread_id=thread_id, - thread=None, + thread=None, # pyrefly: ignore[bad-argument-type] # filled in by Chat adapter=self, raw=activity, ), @@ -503,7 +505,7 @@ def _handle_reaction_activity( user=user, message_id=message_id, thread_id=thread_id, - thread=None, + thread=None, # pyrefly: ignore[bad-argument-type] # filled in by Chat adapter=self, raw=activity, ), @@ -520,7 +522,7 @@ def _handle_reaction_activity( user=user, message_id=message_id, thread_id=thread_id, - thread=None, + thread=None, # pyrefly: ignore[bad-argument-type] # filled in by Chat adapter=self, raw=activity, ), @@ -570,7 +572,7 @@ def _parse_teams_message( def _create_attachment(self, att: dict[str, Any]) -> Attachment: """Create an Attachment from a Teams attachment dict.""" content_type = att.get("contentType", "") - att_type: str = "file" + att_type: Literal["audio", "file", "image", "video"] = "file" if content_type.startswith("image/"): att_type = "image" elif content_type.startswith("video/"): @@ -767,23 +769,23 @@ async def delete_message(self, thread_id: str, message_id: str) -> None: async def add_reaction( self, - _thread_id: str, - _message_id: str, - _emoji: EmojiValue | str, + thread_id: str, + message_id: str, + emoji: EmojiValue | str, ) -> None: """Add a reaction (not supported by Teams Bot Framework API).""" self._logger.warn("addReaction is not supported by the Teams Bot Framework API") async def remove_reaction( self, - _thread_id: str, - _message_id: str, - _emoji: EmojiValue | str, + thread_id: str, + message_id: str, + emoji: EmojiValue | str, ) -> None: """Remove a reaction (not supported by Teams Bot Framework API).""" self._logger.warn("removeReaction is not supported by the Teams Bot Framework API") - async def start_typing(self, thread_id: str, _status: str | None = None) -> None: + async def start_typing(self, thread_id: str, status: str | None = None) -> None: """Send typing indicator to a Teams conversation.""" decoded = self.decode_thread_id(thread_id) @@ -1766,23 +1768,35 @@ async def _verify_bot_framework_token(self, request: Any) -> Any | None: # Request/Response helpers (framework-agnostic) # ========================================================================= - async def _get_request_body(self, request: Any) -> str: + @staticmethod + async def _get_request_body(request: Any) -> str: """Extract the request body as a string.""" - if hasattr(request, "body"): - body = request.body + # `hasattr` narrows `Any` → `object` (not awaitable); using + # `getattr(..., None)` preserves `Any` for framework duck-typing. + # Handle both callable and non-callable `request.text`. Gating + # entry on callability would drop populated string attributes. + text_attr = getattr(request, "text", None) + if text_attr is not None: + if callable(text_attr): + result = text_attr() + text_attr = await result if inspect.isawaitable(result) else result + return text_attr.decode("utf-8") if isinstance(text_attr, (bytes, bytearray)) else str(text_attr) + body = getattr(request, "body", None) + if body is not None: if callable(body): body = body() + # Some frameworks expose `body` as an async method; if calling it + # produced a coroutine, await it before treating as bytes/str. + if inspect.isawaitable(body): + body = await body if hasattr(body, "read"): - raw = await body.read() if hasattr(body.read, "__await__") else body.read() - return raw.decode("utf-8") if isinstance(raw, bytes) else raw - return body.decode("utf-8") if isinstance(body, bytes) else str(body) - if hasattr(request, "text"): - if callable(request.text): - return await request.text() - return request.text - if hasattr(request, "data"): - data = request.data - return data.decode("utf-8") if isinstance(data, bytes) else str(data) + raw_result = body.read() + raw = await raw_result if inspect.isawaitable(raw_result) else raw_result + return raw.decode("utf-8") if isinstance(raw, (bytes, bytearray)) else str(raw) + return body.decode("utf-8") if isinstance(body, (bytes, bytearray)) else str(body) + data = getattr(request, "data", None) + if data is not None: + return data.decode("utf-8") if isinstance(data, (bytes, bytearray)) else str(data) return "" def _get_header(self, request: Any, name: str) -> str | None: diff --git a/src/chat_sdk/adapters/teams/cards.py b/src/chat_sdk/adapters/teams/cards.py index d3c42a3..2346504 100644 --- a/src/chat_sdk/adapters/teams/cards.py +++ b/src/chat_sdk/adapters/teams/cards.py @@ -6,7 +6,7 @@ from __future__ import annotations -from typing import Any +from typing import Any, cast from chat_sdk.cards import ( ActionsElement, @@ -142,8 +142,8 @@ def _convert_child_to_adaptive(child: CardChild) -> dict[str, Any]: if child_type == "fields": return {"elements": [_convert_fields_to_element(child)], "actions": []} # type: ignore[arg-type] if child_type == "link": - label = child.get("label", "") # type: ignore[union-attr] - url = child.get("url", "") # type: ignore[union-attr] + label = cast("str", child.get("label", "")) + url = cast("str", child.get("url", "")) return { "elements": [ { diff --git a/src/chat_sdk/adapters/telegram/adapter.py b/src/chat_sdk/adapters/telegram/adapter.py index 441f3b3..6721751 100644 --- a/src/chat_sdk/adapters/telegram/adapter.py +++ b/src/chat_sdk/adapters/telegram/adapter.py @@ -12,13 +12,14 @@ import asyncio import contextlib import hmac +import inspect import json import math import os import re from dataclasses import dataclass from datetime import datetime, timezone -from typing import Any +from typing import Any, cast from chat_sdk.adapters.telegram.cards import ( card_to_telegram_inline_keyboard, @@ -68,6 +69,7 @@ FetchOptions, FetchResult, FormattedContent, + LockScope, Message, MessageMetadata, RawMessage, @@ -257,7 +259,7 @@ def __init__(self, config: TelegramAdapterConfig | None = None) -> None: ) self._name: str = "telegram" - self._lock_scope: str = "channel" + self._lock_scope: LockScope = "channel" self._persist_message_history: bool = True self._bot_token: str = bot_token @@ -300,7 +302,7 @@ def name(self) -> str: return self._name @property - def lock_scope(self) -> str: + def lock_scope(self) -> LockScope: return self._lock_scope @property @@ -696,11 +698,14 @@ def handle_callback_query( ) decoded = decode_telegram_callback_data(callback_query.get("data")) - action_id = decoded["action_id"] + action_id = decoded["action_id"] or "" value = decoded["value"] # The TS source uses callback_query.from – in our types this is from_user - from_user = callback_query.get("from_user") or callback_query.get("from") # type: ignore[call-overload] + from_user = cast( + "TelegramUser | None", + callback_query.get("from_user") or callback_query.get("from"), # type: ignore[call-overload] + ) user = ( self.to_author(from_user) if from_user @@ -716,7 +721,7 @@ def handle_callback_query( self._chat.process_action( ActionEvent( adapter=self, - thread=None, + thread=None, # pyrefly: ignore[bad-argument-type] # filled in by Chat thread_id=thread_id, message_id=message_id, user=user, @@ -781,7 +786,7 @@ def handle_message_reaction_update( self._chat.process_reaction( ReactionEvent( adapter=self, - thread=None, + thread=None, # pyrefly: ignore[bad-argument-type] # filled in by Chat thread_id=thread_id, message_id=message_id, user=actor, @@ -799,7 +804,7 @@ def handle_message_reaction_update( self._chat.process_reaction( ReactionEvent( adapter=self, - thread=None, + thread=None, # pyrefly: ignore[bad-argument-type] # filled in by Chat thread_id=thread_id, message_id=message_id, user=actor, @@ -1018,7 +1023,7 @@ async def remove_reaction( self, thread_id: str, message_id: str, - _emoji: EmojiValue | str, + emoji: EmojiValue | str, ) -> None: """Remove a reaction from a Telegram message.""" parsed_thread = self._resolve_thread_id(thread_id) @@ -1232,7 +1237,10 @@ def parse_telegram_message( text = apply_telegram_entities(plain_text, entities) # Determine author -- Telegram uses 'from' key which is a reserved word - from_user = raw.get("from_user") or raw.get("from") # type: ignore[call-overload] + from_user = cast( + "TelegramUser | None", + raw.get("from_user") or raw.get("from"), # type: ignore[call-overload] + ) sender_chat = raw.get("sender_chat") if from_user: @@ -1724,15 +1732,17 @@ def to_telegram_reaction(self, emoji: EmojiValue | str) -> TelegramReactionType: def reaction_key(self, reaction: TelegramReactionType) -> str: """Compute a unique key for a Telegram reaction.""" + # `TelegramReactionType` is a TypedDict union; `.get()` returns the + # union of all value-types, so narrow the strings we know are strings. if reaction.get("type") == "emoji": - return reaction.get("emoji", "") - return f"custom:{reaction.get('custom_emoji_id', '')}" + return cast("str", reaction.get("emoji", "")) + return f"custom:{cast('str', reaction.get('custom_emoji_id', ''))}" def reaction_to_emoji_value(self, reaction: TelegramReactionType) -> EmojiValue: """Convert a Telegram reaction to an :class:`EmojiValue`.""" if reaction.get("type") == "emoji": - return get_emoji(reaction.get("emoji", "")) - return get_emoji(f"custom:{reaction.get('custom_emoji_id', '')}") + return get_emoji(cast("str", reaction.get("emoji", ""))) + return get_emoji(f"custom:{cast('str', reaction.get('custom_emoji_id', ''))}") # -- Telegram API -------------------------------------------------------- @@ -1909,13 +1919,22 @@ def _resolve_thread_id(self, value: str) -> TelegramThreadId: @staticmethod async def _get_request_body(request: Any) -> str: """Extract body text from a framework-agnostic request object.""" - if hasattr(request, "text") and callable(request.text): - return await request.text() - if hasattr(request, "body"): - body = request.body + # `hasattr` narrows `Any` → `object` (not awaitable); `getattr(..., None)` + # preserves `Any` for the duck-typed framework paths. + # Handle both callable and non-callable `request.text` forms. + # Gating entry on callability would drop populated string attrs. + text_attr = getattr(request, "text", None) + if text_attr is not None: + if callable(text_attr): + result = text_attr() + text_attr = await result if inspect.isawaitable(result) else result + return text_attr.decode("utf-8") if isinstance(text_attr, (bytes, bytearray)) else str(text_attr) + body = getattr(request, "body", None) + if body is not None: if callable(body): - body = await body() - if isinstance(body, bytes): + result = body() + body = await result if inspect.isawaitable(result) else result + if isinstance(body, (bytes, bytearray)): return body.decode("utf-8") return str(body) return "" diff --git a/src/chat_sdk/adapters/whatsapp/adapter.py b/src/chat_sdk/adapters/whatsapp/adapter.py index 299d9b6..983bfb0 100644 --- a/src/chat_sdk/adapters/whatsapp/adapter.py +++ b/src/chat_sdk/adapters/whatsapp/adapter.py @@ -10,16 +10,22 @@ import hashlib import hmac +import inspect import json import math import os import time from collections.abc import AsyncIterable from datetime import datetime, timezone -from typing import Any +from typing import Any, cast from urllib.parse import parse_qs, urlparse -from chat_sdk.adapters.whatsapp.cards import card_to_whatsapp, decode_whatsapp_callback_data +from chat_sdk.adapters.whatsapp.cards import ( + WhatsAppCardResultInteractive, + WhatsAppCardResultText, + card_to_whatsapp, + decode_whatsapp_callback_data, +) from chat_sdk.adapters.whatsapp.format_converter import WhatsAppFormatConverter from chat_sdk.adapters.whatsapp.types import ( WhatsAppAdapterConfig, @@ -44,8 +50,10 @@ FetchOptions, FetchResult, FormattedContent, + LockScope, Message, MessageMetadata, + PostableMarkdown, RawMessage, ReactionEvent, StreamChunk, @@ -102,7 +110,7 @@ class WhatsAppAdapter: def __init__(self, config: WhatsAppAdapterConfig) -> None: self._name = "whatsapp" - self._lock_scope = "channel" + self._lock_scope: LockScope = "channel" self._persist_message_history = True self._user_name = config.user_name self._access_token = config.access_token @@ -124,7 +132,7 @@ def name(self) -> str: return self._name @property - def lock_scope(self) -> str: + def lock_scope(self) -> LockScope: return self._lock_scope @property @@ -202,12 +210,14 @@ async def handle_webhook( value = change.get("value", {}) - # Process incoming messages + # Process incoming messages. `value["messages"]` is typed as + # `list[dict[str, Any]]` on the webhook TypedDict; cast each + # entry to the more-specific inbound shape for handler dispatch. if value.get("messages"): for message in value["messages"]: try: self._handle_inbound_message( - message, + cast("WhatsAppInboundMessage", message), (value.get("contacts") or [None])[0], value.get("metadata", {}).get("phone_number_id", ""), options, @@ -348,7 +358,7 @@ def _handle_reaction( self._chat.process_reaction( ReactionEvent( adapter=self, - thread=None, + thread=None, # pyrefly: ignore[bad-argument-type] # filled in by Chat thread_id=thread_id, message_id=inbound["reaction"]["message_id"], user=user, @@ -392,14 +402,14 @@ def _handle_interactive_reply( return decoded = decode_whatsapp_callback_data(raw_id) - action_id = decoded["action_id"] + action_id = decoded["action_id"] or "" value = decoded.get("value") if decoded.get("value") is not None else fallback_value contact_name = (contact or {}).get("profile", {}).get("name", "") or inbound["from"] self._chat.process_action( ActionEvent( adapter=self, - thread=None, + thread=None, # pyrefly: ignore[bad-argument-type] # filled in by Chat thread_id=thread_id, message_id=inbound["id"], user=Author( @@ -438,7 +448,7 @@ def _handle_button_response( self._chat.process_action( ActionEvent( adapter=self, - thread=None, + thread=None, # pyrefly: ignore[bad-argument-type] # filled in by Chat thread_id=thread_id, message_id=inbound["id"], user=Author( @@ -708,14 +718,19 @@ async def post_message( # Check if this is a card with interactive buttons card = extract_card(message) if card: + # `card_to_whatsapp` returns a `WhatsAppCardResultInteractive` + # or `WhatsAppCardResultText` union; the `type` check narrows + # at runtime but pyrefly doesn't propagate TypedDict `type`-tag + # discrimination, so cast the field access on each branch. result = card_to_whatsapp(card) if result.get("type") == "interactive": - interactive = json.loads(convert_emoji_placeholders(json.dumps(result["interactive"]), "whatsapp")) + interactive_raw = cast(WhatsAppCardResultInteractive, result)["interactive"] + interactive = json.loads(convert_emoji_placeholders(json.dumps(interactive_raw), "whatsapp")) return await self._send_interactive_message(thread_id, user_wa_id, interactive) return await self._send_text_message( thread_id, user_wa_id, - convert_emoji_placeholders(result["text"], "whatsapp"), + convert_emoji_placeholders(cast(WhatsAppCardResultText, result)["text"], "whatsapp"), ) # Regular text message @@ -840,7 +855,7 @@ async def stream( accumulated += chunk elif hasattr(chunk, "type") and chunk.type == "markdown_text": accumulated += chunk.text - return await self.post_message(thread_id, {"markdown": accumulated}) + return await self.post_message(thread_id, PostableMarkdown(markdown=accumulated)) async def delete_message(self, thread_id: str, message_id: str) -> None: """Delete a message. Not supported by WhatsApp Cloud API.""" @@ -1040,11 +1055,25 @@ def _resolve_emoji(self, emoji: EmojiValue | str) -> str: @staticmethod async def _get_request_body(request: Any) -> str: """Extract body text from a request object.""" - if hasattr(request, "text") and callable(request.text): - return await request.text() - if hasattr(request, "body"): - body = request.body - if isinstance(body, bytes): + # `hasattr` narrows `Any` → `object` (not awaitable); using + # `getattr(..., None)` preserves `Any` for framework duck-typing. + # Handle both callable and non-callable `request.text`. Gating + # entry on callability would drop populated string attributes. + text_attr = getattr(request, "text", None) + if text_attr is not None: + if callable(text_attr): + result = text_attr() + text_attr = await result if inspect.isawaitable(result) else result + return text_attr.decode("utf-8") if isinstance(text_attr, (bytes, bytearray)) else str(text_attr) + body = getattr(request, "body", None) + if body is not None: + # Some frameworks expose `body` as an async method; call and + # await if needed before treating as bytes/str. + if callable(body): + body = body() + if inspect.isawaitable(body): + body = await body + if isinstance(body, (bytes, bytearray)): return body.decode("utf-8") return str(body) return "" diff --git a/src/chat_sdk/adapters/whatsapp/cards.py b/src/chat_sdk/adapters/whatsapp/cards.py index 2c1fd94..971157e 100644 --- a/src/chat_sdk/adapters/whatsapp/cards.py +++ b/src/chat_sdk/adapters/whatsapp/cards.py @@ -12,7 +12,7 @@ from __future__ import annotations import json -from typing import Any, Literal, TypedDict +from typing import Any, Literal, TypedDict, cast from chat_sdk.adapters.whatsapp.types import WhatsAppInteractiveMessage from chat_sdk.cards import ( @@ -224,8 +224,8 @@ def _render_child(child: CardChild) -> list[str]: return result if child_type == "image": - alt = child.get("alt", "") # type: ignore[union-attr] - url = child.get("url", "") # type: ignore[union-attr] + alt = cast("str", child.get("alt", "")) + url = cast("str", child.get("url", "")) if alt: return [f"{alt}: {url}"] return [url] diff --git a/src/chat_sdk/adapters/whatsapp/types.py b/src/chat_sdk/adapters/whatsapp/types.py index 31e21b0..551920d 100644 --- a/src/chat_sdk/adapters/whatsapp/types.py +++ b/src/chat_sdk/adapters/whatsapp/types.py @@ -132,44 +132,49 @@ class WhatsAppWebhookPayload(TypedDict): # ============================================================================= -class WhatsAppInboundMessage(TypedDict, total=False): - """Inbound message from a user. - - See: https://developers.facebook.com/docs/whatsapp/cloud-api/webhooks/payload-examples - """ - - # Audio message content - audio: dict[str, Any] - # Legacy button response (from template quick replies) - button: dict[str, str] - # Context for quoted replies - context: dict[str, str] - # Document message content - document: dict[str, Any] - # Sender's WhatsApp ID - from_: str # mapped from "from" in JSON - # Unique message ID - id: str - # Image message content - image: dict[str, Any] - # Interactive message reply - interactive: dict[str, Any] - # Location message content - location: dict[str, Any] - # Reaction to a message - reaction: dict[str, str] - # Sticker message content - sticker: dict[str, Any] - # Text message content - text: dict[str, str] - # Unix timestamp string - timestamp: str - # Message type - type: str - # Video message content - video: dict[str, Any] - # Voice message content - voice: dict[str, Any] +# Inbound message from a user. The `"from"` field name matches the raw JSON +# key (a Python keyword at class-body level, so we use the functional +# TypedDict form to preserve it verbatim). +# +# See: https://developers.facebook.com/docs/whatsapp/cloud-api/webhooks/payload-examples +WhatsAppInboundMessage = TypedDict( + "WhatsAppInboundMessage", + { + # Audio message content + "audio": dict[str, Any], + # Legacy button response (from template quick replies) + "button": dict[str, str], + # Context for quoted replies + "context": dict[str, str], + # Document message content + "document": dict[str, Any], + # Sender's WhatsApp ID + "from": str, + # Unique message ID + "id": str, + # Image message content + "image": dict[str, Any], + # Interactive message reply + "interactive": dict[str, Any], + # Location message content + "location": dict[str, Any], + # Reaction to a message + "reaction": dict[str, str], + # Sticker message content + "sticker": dict[str, Any], + # Text message content + "text": dict[str, str], + # Unix timestamp string + "timestamp": str, + # Message type + "type": str, + # Video message content + "video": dict[str, Any], + # Voice message content + "voice": dict[str, Any], + }, + total=False, +) # ============================================================================= @@ -254,7 +259,7 @@ class WhatsAppRawMessage(TypedDict, total=False): """ # The raw inbound message data - message: dict[str, Any] + message: WhatsAppInboundMessage # Phone number ID that received the message phone_number_id: str # Contact info from the webhook diff --git a/src/chat_sdk/chat.py b/src/chat_sdk/chat.py index 1e29972..3d5c10d 100644 --- a/src/chat_sdk/chat.py +++ b/src/chat_sdk/chat.py @@ -11,14 +11,14 @@ import asyncio import contextvars import dataclasses +import inspect import re import uuid from collections.abc import Awaitable, Callable -from dataclasses import dataclass from datetime import datetime, timezone from typing import Any -from chat_sdk.channel import ChannelImpl +from chat_sdk.channel import ChannelImpl, _ChannelImplConfigWithAdapter from chat_sdk.errors import ChatError, LockError from chat_sdk.logger import ConsoleLogger, Logger from chat_sdk.thread import ( @@ -36,6 +36,7 @@ AssistantContextChangedEvent, AssistantThreadStartedEvent, Author, + Channel, ChannelVisibility, ChatConfig, ConcurrencyStrategy, @@ -873,7 +874,7 @@ async def process_modal_submit( for pat in self._modal_submit_handlers: if not pat.callback_ids or event.callback_id in pat.callback_ids: try: - response = await pat.handler(full_event) + response = await self._invoke_handler(pat.handler, full_event) if response is not None: return response except Exception as exc: @@ -908,7 +909,7 @@ async def _task() -> None: for pat in self._modal_close_handlers: if not pat.callback_ids or event.callback_id in pat.callback_ids: - await pat.handler(full_event) + await self._invoke_handler(pat.handler, full_event) task = _create_task(_task(), self._active_tasks) if task is not None: @@ -947,7 +948,7 @@ def process_assistant_thread_started( ) -> None: async def _task() -> None: for h in self._assistant_thread_started_handlers: - await h(event) + await self._invoke_handler(h, event) task = _create_task(_task(), self._active_tasks) if task is not None: @@ -968,7 +969,7 @@ def process_assistant_context_changed( ) -> None: async def _task() -> None: for h in self._assistant_context_changed_handlers: - await h(event) + await self._invoke_handler(h, event) task = _create_task(_task(), self._active_tasks) if task is not None: @@ -989,7 +990,7 @@ def process_app_home_opened( ) -> None: async def _task() -> None: for h in self._app_home_opened_handlers: - await h(event) + await self._invoke_handler(h, event) task = _create_task(_task(), self._active_tasks) if task is not None: @@ -1010,7 +1011,7 @@ def process_member_joined_channel( ) -> None: async def _task() -> None: for h in self._member_joined_channel_handlers: - await h(event) + await self._invoke_handler(h, event) task = _create_task(_task(), self._active_tasks) if task is not None: @@ -1046,7 +1047,7 @@ async def _handle_slash_command_event(self, event: SlashCommandEvent) -> None: # Create channel for the command channel_id = getattr(event, "channel_id", None) or (event.channel.id if event.channel else "") channel = ChannelImpl( - _ChannelImplConfigForChat( + _ChannelImplConfigWithAdapter( id=channel_id, adapter=event.adapter, state_adapter=self._state_adapter, @@ -1080,11 +1081,11 @@ async def _open_modal(modal: Any) -> dict[str, str] | None: for pat in self._slash_command_handlers: if not pat.commands: self._logger.debug("Running catch-all slash command handler") - await pat.handler(full_event) + await self._invoke_handler(pat.handler, full_event) continue if event.command in pat.commands: self._logger.debug("Running matched slash command handler", {"command": event.command}) - await pat.handler(full_event) + await self._invoke_handler(pat.handler, full_event) # ======================================================================== # Modal context persistence @@ -1096,7 +1097,7 @@ def _store_modal_context( context_id: str, thread: ThreadImpl | None = None, message: Message | None = None, - channel: ChannelImpl | None = None, + channel: Channel | None = None, ) -> None: key = f"modal-context:{adapter_name}:{context_id}" context = { @@ -1255,11 +1256,11 @@ async def _open_modal(modal: Any) -> dict[str, str] | None: for pat in self._action_handlers: if not pat.action_ids: self._logger.debug("Running catch-all action handler") - await pat.handler(full_event) + await self._invoke_handler(pat.handler, full_event) continue if event.action_id in pat.action_ids: self._logger.debug("Running matched action handler", {"action_id": event.action_id}) - await pat.handler(full_event) + await self._invoke_handler(pat.handler, full_event) # ======================================================================== # Reaction handling @@ -1317,7 +1318,7 @@ async def _handle_reaction_event(self, event: ReactionEvent) -> None: for pat in self._reaction_handlers: if not pat.emoji: self._logger.debug("Running catch-all reaction handler") - await pat.handler(full_event) + await self._invoke_handler(pat.handler, full_event) continue matches = any( @@ -1333,7 +1334,7 @@ async def _handle_reaction_event(self, event: ReactionEvent) -> None: ) if matches: self._logger.debug("Running matched reaction handler") - await pat.handler(full_event) + await self._invoke_handler(pat.handler, full_event) # ======================================================================== # openDM / channel @@ -1371,7 +1372,7 @@ def channel(self, channel_id: str) -> ChannelImpl: if adapter is None: raise ChatError(f'Adapter "{adapter_name}" not found for channel ID "{channel_id}"') return ChannelImpl( - _ChannelImplConfigForChat( + _ChannelImplConfigWithAdapter( id=channel_id, adapter=adapter, state_adapter=self._state_adapter, @@ -1504,7 +1505,11 @@ async def _get_lock_key(self, adapter: Adapter, thread_id: str) -> str: if hasattr(adapter, "is_dm") and callable(getattr(adapter, "is_dm", None)) else False ) # type: ignore[union-attr] - scope = await self._lock_scope_config( + # The public contract lets callers return either `LockScope` (sync) + # or `Awaitable[LockScope]`. `inspect.isawaitable` narrows so we + # only `await` the coroutine/future branch — doing so + # unconditionally would raise `TypeError` on a sync return. + result = self._lock_scope_config( LockScopeContext( adapter=adapter, channel_id=channel_id, @@ -1512,6 +1517,7 @@ async def _get_lock_key(self, adapter: Adapter, thread_id: str) -> str: thread_id=thread_id, ) ) + scope = await result if inspect.isawaitable(result) else result else: scope = self._lock_scope_config or adapter.lock_scope or "thread" # type: ignore[assignment] @@ -1868,7 +1874,9 @@ async def _dispatch_to_handlers( self._logger.debug("Direct message received - calling handlers", {"thread_id": thread_id}) channel = thread.channel for h in self._direct_message_handlers: - await h(thread, message, channel, context) + result = h(thread, message, channel, context) + if inspect.isawaitable(result): + await result return # Backward compat: DMs without handlers treated as mentions @@ -1893,7 +1901,9 @@ async def _dispatch_to_handlers( if pat.pattern.search(message.text): self._logger.debug("Message matched pattern", {"pattern": pat.pattern.pattern}) matched = True - await pat.handler(thread, message, context) + result = pat.handler(thread, message, context) + if inspect.isawaitable(result): + await result if not matched: self._logger.debug("No handlers matched message", {"thread_id": thread_id}) @@ -2035,7 +2045,28 @@ async def _run_handlers( context: MessageContext | None = None, ) -> None: for h in handlers: - await h(thread, message, context) + await self._invoke_handler(h, thread, message, context) + + @staticmethod + async def _invoke_handler(handler: Any, /, *args: Any, **kwargs: Any) -> Any: + """Invoke a handler and await the result only if awaitable. + + All Chat handler types (message, reaction, action, slash, modal, + options-load, assistant, home, member-joined) are declared as + `Callable[..., Awaitable[T] | T]` — i.e. users may register either + a sync or an async callable. Awaiting the return value + unconditionally raises `TypeError: object NoneType can't be used + in 'await' expression` for sync handlers, so this helper narrows + with `inspect.isawaitable`. + + Returns whatever the handler returned (post-await for async) so + callers that need the value (modal submit → `ModalResponse`) can + still capture it. + """ + result = handler(*args, **kwargs) + if inspect.isawaitable(result): + return await result + return result # --------------------------------------------------------------------------- @@ -2106,21 +2137,3 @@ async def get_messages(self, thread_id: str, limit: int | None = None) -> list[M if limit is not None: messages = messages[-limit:] return messages - - -# --------------------------------------------------------------------------- -# Config helper used by Chat._create_thread internally -# (avoids importing channel config types at module level) -# --------------------------------------------------------------------------- - - -@dataclass -class _ChannelImplConfigForChat: - """Config passed from Chat to ChannelImpl.""" - - id: str - adapter: Adapter - state_adapter: StateAdapter - channel_visibility: ChannelVisibility = "unknown" - is_dm: bool = False - message_history: Any = None diff --git a/src/chat_sdk/shared/base_format_converter.py b/src/chat_sdk/shared/base_format_converter.py index 31f3805..0f6f7e4 100644 --- a/src/chat_sdk/shared/base_format_converter.py +++ b/src/chat_sdk/shared/base_format_converter.py @@ -15,7 +15,7 @@ from abc import ABC, abstractmethod from collections.abc import Callable -from typing import Any +from typing import Any, cast from chat_sdk.shared.markdown_parser import ( Content, @@ -186,12 +186,14 @@ def render_postable(self, message: PostableMessageInput) -> str: return card_to_fallback_text(message["card"]) if message.get("type") == "card": - from chat_sdk.cards import is_card_element + from chat_sdk.cards import CardElement, is_card_element if is_card_element(message): from chat_sdk.cards import card_to_fallback_text - return card_to_fallback_text(message) + # `is_card_element` isn't a TypeGuard, so pyrefly still + # sees `dict[Any, Any]`. Cast to match the signature. + return card_to_fallback_text(cast("CardElement", message)) return "" return str(message) diff --git a/src/chat_sdk/shared/card_utils.py b/src/chat_sdk/shared/card_utils.py index 2552da8..397b0e9 100644 --- a/src/chat_sdk/shared/card_utils.py +++ b/src/chat_sdk/shared/card_utils.py @@ -3,7 +3,7 @@ from __future__ import annotations from collections.abc import Callable -from typing import Literal +from typing import Literal, cast from chat_sdk.cards import CardChild, CardElement, card_child_to_fallback_text, table_element_to_ascii from chat_sdk.emoji import convert_emoji_placeholders @@ -62,20 +62,30 @@ def card_to_fallback_text( def _child_to_fallback_text(child: CardChild, convert_text: Callable[[str], str]) -> str | None: - """Convert a card child element to fallback text.""" + """Convert a card child element to fallback text. + + `child` is a `CardChild` union of TypedDicts; the `child_type` check + narrows at runtime but pyrefly returns `object | str` from `.get()` + on the union, so we `cast` on each branch to the specific value type. + """ child_type = child.get("type", "") if child_type == "text": - return convert_text(child.get("content", "")) + return convert_text(cast("str", child.get("content", ""))) if child_type == "link": - return f"{convert_text(child.get('label', ''))} ({child.get('url', '')})" + return f"{convert_text(cast('str', child.get('label', '')))} ({cast('str', child.get('url', ''))})" if child_type == "fields": - return "\n".join(f"{convert_text(f['label'])}: {convert_text(f['value'])}" for f in child.get("children", [])) + children = cast("list[dict[str, str]]", child.get("children", [])) + return "\n".join(f"{convert_text(f['label'])}: {convert_text(f['value'])}" for f in children) if child_type == "actions": return None if child_type == "section": - return "\n".join(filter(None, (_child_to_fallback_text(c, convert_text) for c in child.get("children", [])))) + section_children = cast("list[CardChild]", child.get("children", [])) + return "\n".join(filter(None, (_child_to_fallback_text(c, convert_text) for c in section_children))) if child_type == "table": - return table_element_to_ascii(child.get("headers", []), child.get("rows", [])) + return table_element_to_ascii( + cast("list[str]", child.get("headers", [])), + cast("list[list[str]]", child.get("rows", [])), + ) if child_type == "divider": return "---" return card_child_to_fallback_text(child) diff --git a/src/chat_sdk/shared/mock_adapter.py b/src/chat_sdk/shared/mock_adapter.py index bc29b59..ba75254 100644 --- a/src/chat_sdk/shared/mock_adapter.py +++ b/src/chat_sdk/shared/mock_adapter.py @@ -190,7 +190,10 @@ async def fetch_channel_info(self, channel_id: str) -> ChannelInfo: ) async def post_channel_message(self, channel_id: str, message: AdapterPostableMessage) -> RawMessage: - return RawMessage(id="msg-1", thread_id=None, raw={}) + # Channel-scoped posts don't have a thread — use the channel id as + # the identifier so downstream code that expects a non-null + # `thread_id` can still route by string key. + return RawMessage(id="msg-1", thread_id=channel_id, raw={}) # --------------------------------------------------------------------------- diff --git a/src/chat_sdk/thread.py b/src/chat_sdk/thread.py index 461d344..7702fa1 100644 --- a/src/chat_sdk/thread.py +++ b/src/chat_sdk/thread.py @@ -12,7 +12,7 @@ from collections.abc import AsyncIterator from dataclasses import dataclass from datetime import datetime, timezone -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING, Any, Protocol, cast, runtime_checkable from chat_sdk.errors import ChatNotImplementedError from chat_sdk.logger import Logger @@ -24,13 +24,16 @@ AdapterPostableMessage, Attachment, Author, + Channel, ChannelVisibility, EmojiValue, EphemeralMessage, FetchOptions, FormattedContent, + MarkdownTextChunk, Message, MessageMetadata, + PlanUpdateChunk, PostableCard, PostableMarkdown, PostableMessage, @@ -42,6 +45,7 @@ StateAdapter, StreamChunk, StreamOptions, + TaskUpdateChunk, ) if TYPE_CHECKING: @@ -56,8 +60,15 @@ _active_chat: contextvars.ContextVar[_ChatSingleton | None] = contextvars.ContextVar("_active_chat", default=None) -class _ChatSingleton: - """Minimal interface for the Chat singleton to avoid circular imports.""" +@runtime_checkable +class _ChatSingleton(Protocol): + """Structural interface for the Chat singleton. + + Declared as a `Protocol` so the concrete `Chat` class structurally + satisfies it without an explicit import/inheritance cycle — both + `thread.py` and `channel.py` need the type but can't directly depend + on `chat.py`. + """ def get_adapter(self, name: str) -> Adapter | None: ... def get_state(self) -> StateAdapter: ... @@ -333,8 +344,14 @@ async def set_state( # -- Channel ------------------------------------------------------------- @property - def channel(self) -> ChannelImpl: - """Get the Channel containing this thread. Lazy-created and cached.""" + def channel(self) -> Channel: + """Get the Channel containing this thread. Lazy-created and cached. + + Declared as `Channel` (the protocol) rather than `ChannelImpl` so + `ThreadImpl` structurally satisfies the `Thread` protocol — + property getter return types have to match protocol return types + exactly. + """ if self._channel_cache is None: from chat_sdk.channel import ChannelImpl, derive_channel_id @@ -575,7 +592,10 @@ async def _text_only_stream() -> AsyncIterator[str]: elif isinstance(chunk, dict) and chunk.get("type") == "markdown_text": yield chunk.get("text", "") elif hasattr(chunk, "type") and getattr(chunk, "type", None) == "markdown_text": - yield chunk.text + # Runtime-narrowed to a MarkdownTextChunk via the `type` + # tag; only that variant has `.text`. Pyrefly doesn't do + # tag-based union narrowing, so read via `getattr`. + yield getattr(chunk, "text", "") # Skip non-text chunks in fallback mode return await self._fallback_stream(_text_only_stream(), options) @@ -1124,9 +1144,12 @@ async def _from_full_stream(raw_stream: Any) -> AsyncIterator[str | StreamChunk] elif isinstance(item, dict): t = item.get("type") - # Pass through known StreamChunk dict types + # Pass through known StreamChunk dict types. The `t` check + # narrows at runtime to one of the three StreamChunk TypedDicts, + # but pyrefly doesn't narrow through tag-string comparisons, so + # cast to the declared yield union. if t in ("markdown_text", "task_update", "plan_update"): - yield item + yield cast("MarkdownTextChunk | PlanUpdateChunk | TaskUpdateChunk", item) continue if t == "text-delta": diff --git a/src/chat_sdk/types.py b/src/chat_sdk/types.py index e4386a4..439d7e2 100644 --- a/src/chat_sdk/types.py +++ b/src/chat_sdk/types.py @@ -944,7 +944,15 @@ class PostEphemeralOptions: @dataclass class ReactionEvent: - """Reaction event data.""" + """Reaction event data. + + Matches upstream TS `ReactionEvent` shape: `thread` is required here + because handlers receive the fully-populated event after `Chat` + re-wraps any partial event from an adapter. Adapters dispatch via + `chat.process_reaction(...)` with a partial event (`thread=None` at + construction); `Chat` resolves the real thread before invoking + handlers, so this field is never `None` at handler time. + """ adapter: Adapter thread: Thread @@ -1021,7 +1029,14 @@ class ModalResponse: @dataclass class SlashCommandEvent: - """Slash command event data.""" + """Slash command event data. + + Matches upstream TS `SlashCommandEvent`: `channel` is required here + because handlers receive the fully-populated event after `Chat` + re-wraps the partial event from an adapter. Adapters pass + `channel=None` at construction; `Chat` constructs a real `Channel` + before invoking handlers, so this is never `None` at handler time. + """ adapter: Adapter channel: Channel @@ -1326,6 +1341,7 @@ def process_message( message: Message | Callable[[], Awaitable[Message]], options: WebhookOptions | None = None, ) -> None: ... + async def handle_incoming_message(self, adapter: Adapter, thread_id: str, message: Message) -> None: ... def process_action(self, event: Any, options: WebhookOptions | None = None) -> None: ... def process_reaction(self, event: Any, options: WebhookOptions | None = None) -> None: ... def process_slash_command(self, event: Any, options: WebhookOptions | None = None) -> None: ... @@ -1395,11 +1411,15 @@ async def schedule( async def set_state( self, - state: dict[str, Any], + new_state: dict[str, Any], *, replace: bool = False, ) -> None: - """Set the state. Merges with existing state by default.""" + """Set the state. Merges with existing state by default. + + Parameter is named `new_state` to match upstream TS + `setState(newState)` and preserve call-site kwarg compatibility. + """ ... async def start_typing(self, status: str | None = None) -> None: diff --git a/tests/test_coverage_gaps.py b/tests/test_coverage_gaps.py index b148fb7..5fbf098 100644 --- a/tests/test_coverage_gaps.py +++ b/tests/test_coverage_gaps.py @@ -429,7 +429,7 @@ async def text_gen(): adapter.post_message.assert_called_once() call_args = adapter.post_message.call_args assert call_args.args[0] == thread_id - assert call_args.args[1]["raw"] == "hello world" + assert call_args.args[1].raw == "hello world" assert result.id == "comment-1" @pytest.mark.asyncio @@ -463,7 +463,7 @@ async def dict_gen(): await adapter.stream("linear:issue-2", dict_gen()) adapter.post_message.assert_called_once() - assert adapter.post_message.call_args.args[1]["raw"] == "chunk" + assert adapter.post_message.call_args.args[1].raw == "chunk" # =========================================================================== diff --git a/tests/test_critical_fixes.py b/tests/test_critical_fixes.py index b5942d1..9d1baea 100644 --- a/tests/test_critical_fixes.py +++ b/tests/test_critical_fixes.py @@ -323,7 +323,7 @@ async def text_stream(): assert result.id == "msg-1" assert len(posted) == 1 assert posted[0][0] == "gchat:spaces/abc" - assert posted[0][1] == {"markdown": "Hello world"} + assert posted[0][1].markdown == "Hello world" @pytest.mark.asyncio async def test_stream_handles_markdown_text_chunks(self): @@ -349,7 +349,7 @@ async def mixed_stream(): await adapter.stream("gchat:spaces/xyz", mixed_stream()) - assert posted[0][1] == {"markdown": "Start middle end"} + assert posted[0][1].markdown == "Start middle end" # --------------------------------------------------------------------------- @@ -496,7 +496,7 @@ async def text_stream(): assert result.id == "wamid.streamed" assert len(posted) == 1 - assert posted[0][1] == {"markdown": "chunk1 chunk2 chunk3"} + assert posted[0][1].markdown == "chunk1 chunk2 chunk3" # --------------------------------------------------------------------------- diff --git a/tests/test_request_body_extraction.py b/tests/test_request_body_extraction.py new file mode 100644 index 0000000..c8c0454 --- /dev/null +++ b/tests/test_request_body_extraction.py @@ -0,0 +1,225 @@ +"""Regression tests for `_get_request_body` body-extraction across adapters. + +Each adapter's webhook handler normalizes the incoming request object via a +framework-agnostic `_get_request_body` helper. The helper supports multiple +frameworks by duck-typing on `request.text` and `request.body` — each of which +may be: + +- a plain string/bytes attribute (Django raw HttpRequest, some mocks) +- a synchronous method (older frameworks, test helpers) +- an async method / coroutine-returning callable (aiohttp, FastAPI, Starlette) + +Historical bugs in this code path have been: + +1. `hasattr(request, "text") and callable(request.text)` gate silently dropped + non-callable `request.text` string attributes, falling through to `body`. + Fixed across 5 adapters by restructuring the branch. +2. `await request.text()` without `isawaitable` narrowing crashed on Flask-style + sync `text` methods with `TypeError: object is not awaitable`. Fixed by + guarding with `inspect.isawaitable`. +3. `request.body` exposed as an async method (`async def body(self)`) returned + a coroutine that was stringified instead of awaited. Fixed by adding the + same callable + isawaitable dance for `body`. +4. `isinstance(body, bytes)` missed `bytearray`. Fixed by using + `(bytes, bytearray)` consistently. + +Each test below locks in one of those cases per adapter. +""" + +from __future__ import annotations + +from typing import Any + +import pytest + +pytestmark = pytest.mark.asyncio + + +# --------------------------------------------------------------------------- +# Fake request objects covering the shapes we need to support. +# --------------------------------------------------------------------------- + + +class _StringTextRequest: + """`request.text` is a populated string attribute (Django-style).""" + + def __init__(self, body: str) -> None: + self.text = body + + +class _BytesTextRequest: + """`request.text` is populated bytes.""" + + def __init__(self, body: bytes) -> None: + self.text = body + + +class _BytearrayTextRequest: + """`request.text` is a bytearray.""" + + def __init__(self, body: bytearray) -> None: + self.text = body + + +class _SyncCallableTextRequest: + """`request.text` is a sync method (Flask-style).""" + + def __init__(self, body: str) -> None: + self._body = body + + def text(self) -> str: # pragma: no cover - called + return self._body + + +class _AsyncCallableTextRequest: + """`request.text` is an async method (aiohttp/FastAPI).""" + + def __init__(self, body: str) -> None: + self._body = body + + async def text(self) -> str: + return self._body + + +class _AsyncCallableBodyRequest: + """`request.body` is an async method returning bytes.""" + + def __init__(self, body: bytes) -> None: + self._body = body + + async def body(self) -> bytes: + return self._body + + +class _SyncCallableBodyRequest: + """`request.body` is a sync method returning bytes.""" + + def __init__(self, body: bytes) -> None: + self._body = body + + def body(self) -> bytes: # pragma: no cover - called + return self._body + + +class _PropertyBodyRequest: + """`request.body` is a bytes property.""" + + def __init__(self, body: bytes) -> None: + self.body = body + + +class _BytearrayBodyRequest: + """`request.body` is a bytearray.""" + + def __init__(self, body: bytearray) -> None: + self.body = body + + +# --------------------------------------------------------------------------- +# Per-adapter matrix — each adapter exposes `_get_request_body` (static or +# instance). We run the same 7 cases through each to lock in the contract. +# --------------------------------------------------------------------------- + + +def _adapters() -> list[tuple[str, Any]]: + """Return (name, extractor) pairs for each adapter's body extraction. + + Imported directly: every adapter lazy-imports its platform deps inside + methods, so module-level import works in any environment the test runs + in. A broken adapter module fails the suite — matching the repo rule + "every test must fail when the code is wrong." + """ + from chat_sdk.adapters.discord.adapter import DiscordAdapter + from chat_sdk.adapters.github.adapter import GitHubAdapter + from chat_sdk.adapters.linear.adapter import LinearAdapter + from chat_sdk.adapters.teams.adapter import TeamsAdapter + from chat_sdk.adapters.telegram.adapter import TelegramAdapter + from chat_sdk.adapters.whatsapp.adapter import WhatsAppAdapter + + return [ + ("github", GitHubAdapter._get_request_body), + ("telegram", TelegramAdapter._get_request_body), + ("whatsapp", WhatsAppAdapter._get_request_body), + ("discord", DiscordAdapter._get_request_body), + ("linear", LinearAdapter._get_request_body), + ("teams", TeamsAdapter._get_request_body), + ] + + +@pytest.mark.parametrize("name,extractor", _adapters()) +async def test_handles_string_text_attribute(name: str, extractor: Any) -> None: + """Non-callable `request.text` string attribute is consumed — the + historical bug was silently falling through to `body`, which + produced ``-style stringified objects for valid webhooks. + """ + result = await extractor(_StringTextRequest('{"ok": true}')) + assert result == '{"ok": true}', f"{name} failed string text attr" + + +@pytest.mark.parametrize("name,extractor", _adapters()) +async def test_handles_bytes_text_attribute(name: str, extractor: Any) -> None: + """Bytes `request.text` is decoded as UTF-8.""" + result = await extractor(_BytesTextRequest(b'{"ok": true}')) + assert result == '{"ok": true}', f"{name} failed bytes text attr" + + +@pytest.mark.parametrize("name,extractor", _adapters()) +async def test_handles_bytearray_text_attribute(name: str, extractor: Any) -> None: + """Bytearray `request.text` is also decoded — bytes/bytearray symmetry.""" + result = await extractor(_BytearrayTextRequest(bytearray(b'{"ok": true}'))) + assert result == '{"ok": true}', f"{name} failed bytearray text attr" + + +@pytest.mark.parametrize("name,extractor", _adapters()) +async def test_handles_async_callable_text(name: str, extractor: Any) -> None: + """`async def text(self)` returning a coroutine is awaited.""" + result = await extractor(_AsyncCallableTextRequest('{"ok": true}')) + assert result == '{"ok": true}', f"{name} failed async text()" + + +@pytest.mark.parametrize("name,extractor", _adapters()) +async def test_handles_sync_callable_text(name: str, extractor: Any) -> None: + """Sync `def text(self)` returns the body directly — the + historical bug was `await request.text()` crashing on this path. + """ + result = await extractor(_SyncCallableTextRequest('{"ok": true}')) + assert result == '{"ok": true}', f"{name} failed sync text()" + + +@pytest.mark.parametrize("name,extractor", _adapters()) +async def test_handles_async_callable_body(name: str, extractor: Any) -> None: + """Falls back to `request.body` when no `text`. Historical bug: + `async def body(self)` returning a coroutine was stringified as + `` instead of awaited, breaking webhook + signature verification on FastAPI/Starlette. + """ + result = await extractor(_AsyncCallableBodyRequest(b'{"ok": true}')) + assert result == '{"ok": true}', f"{name} failed async body()" + + +@pytest.mark.parametrize("name,extractor", _adapters()) +async def test_handles_sync_callable_body(name: str, extractor: Any) -> None: + """Sync `def body(self)` returning bytes is consumed directly — + covers the callable-but-non-awaitable body branch. Without this, + a regression that always awaits `body()` would only show up in + async-framework use and silently pass the suite. + """ + result = await extractor(_SyncCallableBodyRequest(b'{"ok": true}')) + assert result == '{"ok": true}', f"{name} failed sync body()" + + +@pytest.mark.parametrize("name,extractor", _adapters()) +async def test_handles_bytes_body_attribute(name: str, extractor: Any) -> None: + """`request.body` as a bytes property is decoded as UTF-8.""" + result = await extractor(_PropertyBodyRequest(b'{"ok": true}')) + assert result == '{"ok": true}', f"{name} failed bytes body attr" + + +@pytest.mark.parametrize("name,extractor", _adapters()) +async def test_handles_bytearray_body_attribute(name: str, extractor: Any) -> None: + """`request.body` as bytearray — bytes/bytearray symmetry on the body + path, catching the asymmetry where we fixed text-path for bytearray + but left body-path checking only `isinstance(body, bytes)`. + """ + result = await extractor(_BytearrayBodyRequest(bytearray(b'{"ok": true}'))) + assert result == '{"ok": true}', f"{name} failed bytearray body attr"