diff --git a/.github/workflows/ci-build.yml b/.github/workflows/ci-build.yml new file mode 100644 index 000000000..4a8bd835d --- /dev/null +++ b/.github/workflows/ci-build.yml @@ -0,0 +1,34 @@ +# For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions +name: CI Build + +on: + push: + branches: [ v2 ] + pull_request: + +jobs: + build: + runs-on: ubuntu-latest + timeout-minutes: 10 + strategy: + matrix: + # TODO: 3.10 in GH Actions is not yet stable enough for us + # You may often encounter "Error: Process completed with exit code 250." + python-version: ['3.6', '3.9'] + env: + PYTHON_SLACK_SDK_MOCK_SERVER_MODE: 'threading' + #CI_UNSTABLE_TESTS_SKIP_ENABLED: '1' + steps: + - uses: actions/checkout@v2 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: | + pip install -U pip wheel + pip install -e ".[testing]" + pip install -e ".[optional]" + - name: Run validation + run: | + python setup.py validate diff --git a/.gitignore b/.gitignore index bd5e34fc8..a5ac565f9 100644 --- a/.gitignore +++ b/.gitignore @@ -36,3 +36,4 @@ tmp.txt logs/ .pytype/ +.venv* diff --git a/README.md b/README.md index 9de67ecf1..23e117015 100755 --- a/README.md +++ b/README.md @@ -1,15 +1,17 @@ # Python slackclient +## Important Notice + +**This [`slackclient`](https://pypi.org/project/slackclient/) PyPI project is in maintenance mode now** and [`slack-sdk`](https://pypi.org/project/slack-sdk/) project is the successor. The v3 SDK provides more functionalities such as Socket Mode, OAuth flow module, SCIM API, Audit Logs API, better asyncio support, retry handlers, and many more. + +Refer to [the migration guide](https://slack.dev/python-slack-sdk/v3-migration/index.html#from-slackclient-2-x) to learn how to smoothly migrate your existing code. + +## Overview + The Python `slackclient` library is a developer kit for interfacing with the Slack Web API and Real Time Messaging (RTM) API on Python 3.6 and above. **Comprehensive documentation on using the Slack Python can be found at [https://slack.dev/python-slackclient/](https://slack.dev/python-slackclient/)** -[![pypi package][pypi-image]][pypi-url] -[![Build Status][travis-image]][travis-url] -[![Python Version][python-version]][pypi-url] -[![codecov][codecov-image]][codecov-url] -[![contact][contact-image]][contact-url] - Whether you're building a custom app for your team, or integrating a third party service into your Slack workflows, Slack Developer Kit for Python allows you to leverage the flexibility of Python to get your project up and running as quickly as possible. The **Python slackclient** allows interaction with: @@ -17,7 +19,7 @@ The **Python slackclient** allows interaction with: - The Slack web api methods available at our [Api Docs site][api-methods] - Interaction with our [RTM API][rtm-docs] -If you want to use our [Events API][events-docs], please check the [Slack Events API adapter for Python][python-slack-events-api]. +If you want to use our [Events API][events-docs] and Interactivity features, please check the [Bolt for Python][bolt-python] library. Details on the Tokens and Authentication can be found in our [Auth Guide](https://slack.dev/python-slack-sdk/installation/). Details on the Tokens and Authentication can be found in our [Auth Guide](https://slack.dev/python-slackclient/auth.html). @@ -189,7 +191,7 @@ When in async mode its important to remember to await or run/run_until_complete #### Slackclient as a script -```python +```python import asyncio import os from slack import WebClient @@ -326,10 +328,7 @@ If you're migrating from v1.x of slackclient to v2.x, Please follow our migratio --- -If you get stuck, we’re here to help. The following are the best ways to get assistance working through your issue: - -Use our [Github Issue Tracker][gh-issues] for reporting bugs or requesting features. -Visit the [Slack Community][slack-community] for getting help using Slack Developer Kit for Python or just generally bond with your fellow Slack developers. +We no longer provide support for v1 or v2 of this SDK and only maintain the latest v3 version. If you would like support from this project's maintainers please consider updating to the latest version of the SDK first. Otherwise, you may visit the [Slack Community][slack-community] for getting help using Slack Developer Kit for Python or just generally bond with your fellow Slack developers. diff --git a/setup.py b/setup.py index 175b83bcc..afc801199 100644 --- a/setup.py +++ b/setup.py @@ -16,10 +16,17 @@ with codecs.open(os.path.join(here, "README.md"), encoding="utf-8") as readme: long_description = readme.read() -tests_require = ["pytest>=5,<6", "pytest-cov>=2,<3", "codecov>=2,<3", "flake8>=3,<4", "black", "psutil"] +tests_require = [ + "pytest>=5,<6", + "pytest-cov>=2,<3", + "codecov>=2,<3", + "flake8>=3,<4", + "black", + "psutil", +] -needs_pytest = {'pytest', 'test', 'ptr'}.intersection(sys.argv) -pytest_runner = ['pytest-runner'] if needs_pytest else [] +needs_pytest = {"pytest", "test", "ptr"}.intersection(sys.argv) +pytest_runner = ["pytest-runner"] if needs_pytest else [] class BaseCommand(Command): @@ -88,9 +95,9 @@ class ValidateCommand(BaseCommand): description = "Run Python static code analyzer (flake8), formatter (black) and unit tests (pytest)." user_options = [ - ('unit-test-target=', 'i', 'tests/{unit-test-target}'), - ('utt=', 'i', 'tests/{utt}'), - ('test-target=', 'i', 'tests/{test-target}') + ("unit-test-target=", "i", "tests/{unit-test-target}"), + ("utt=", "i", "tests/{utt}"), + ("test-target=", "i", "tests/{test-target}"), ] def initialize_options(self): @@ -102,27 +109,37 @@ def run(self): with open("./slack/web/client.py", "r") as original: source = original.read() import re - async_source = "# !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\n" \ - "#\n" \ - "# *** DO NOT EDIT THIS FILE ***\n" \ - "#\n" \ - "# 1) Modify slack/web/client.py\n" \ - "# 2) Run `python setup.py validate`\n" \ - "#\n" \ - "# !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\n" \ - "\n" \ - + source + + async_source = ( + "# !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\n" + "#\n" + "# *** DO NOT EDIT THIS FILE ***\n" + "#\n" + "# 1) Modify slack/web/client.py\n" + "# 2) Run `python setup.py validate`\n" + "#\n" + "# !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\n" + "\n" + source + ) async_source = re.sub(" def ", " async def ", async_source) async_source = re.sub("from asyncio import Future\n", "", async_source) - async_source = re.sub("return self.api_call\(", "return await self.api_call(", async_source) - async_source = re.sub("Union\[Future, SlackResponse\]", "AsyncSlackResponse", async_source) + async_source = re.sub( + "return self.api_call\(", "return await self.api_call(", async_source + ) + async_source = re.sub( + "Union\[Future, SlackResponse\]", "AsyncSlackResponse", async_source + ) async_source = re.sub( "from slack.web.base_client import BaseClient, SlackResponse", - "from slack.web.async_base_client import AsyncBaseClient, AsyncSlackResponse", async_source) + "from slack.web.async_base_client import AsyncBaseClient, AsyncSlackResponse", + async_source, + ) async_source = re.sub( "class WebClient\(BaseClient\):", - "class AsyncWebClient(AsyncBaseClient):", async_source) - with open('./slack/web/async_client.py', 'w') as output: + "class AsyncWebClient(AsyncBaseClient):", + async_source, + ) + with open("./slack/web/async_client.py", "w") as output: output.write(async_source) self._run( @@ -132,7 +149,9 @@ def run(self): self._run("Running black…", [sys.executable, "-m", "black", f"{here}/slack"]) self._run("Running flake8…", [sys.executable, "-m", "flake8", f"{here}/slack"]) - target = (self.utt or self.unit_test_target or self.test_target).replace("tests/", "") + target = (self.utt or self.unit_test_target or self.test_target).replace( + "tests/", "" + ) self._run( "Running pytest…", [ @@ -152,9 +171,13 @@ class RunIntegrationTestsCommand(BaseCommand): description = "Run integration tests (pytest)." user_options = [ - ('integration-test-target=', 'i', 'integration_tests/{integration-test-target}'), - ('itt=', 'i', 'integration_tests/{itt}'), - ('test-target=', 'i', 'integration_tests/{test-target}') + ( + "integration-test-target=", + "i", + "integration_tests/{integration-test-target}", + ), + ("itt=", "i", "integration_tests/{itt}"), + ("test-target=", "i", "integration_tests/{test-target}"), ] def initialize_options(self): @@ -163,7 +186,9 @@ def initialize_options(self): self.test_target = "" def run(self): - target = (self.itt or self.integration_test_target or self.test_target).replace("integration_tests/", "") + target = (self.itt or self.integration_test_target or self.test_target).replace( + "integration_tests/", "" + ) self._run( "Running pytest…", [ @@ -183,11 +208,15 @@ class RunAllTestsCommand(ValidateCommand): description = ValidateCommand.description + "\nRun integration tests (pytest)." user_options = [ - ('unit-test-target=', 'i', 'tests/{unit-test-target}'), - ('utt=', 'i', 'tests/{utt}'), - ('integration-test-target=', 'i', 'integration_tests/{integration-test-target}'), - ('itt=', 'i', 'integration_tests/{itt}'), - ('test-target=', 'i', 'integration_tests/{test-target}') + ("unit-test-target=", "i", "tests/{unit-test-target}"), + ("utt=", "i", "tests/{utt}"), + ( + "integration-test-target=", + "i", + "integration_tests/{integration-test-target}", + ), + ("itt=", "i", "integration_tests/{itt}"), + ("test-target=", "i", "integration_tests/{test-target}"), ] def initialize_options(self): @@ -199,7 +228,9 @@ def initialize_options(self): def run(self): ValidateCommand.run(self) - target = (self.itt or self.integration_test_target or self.test_target).replace("integration_tests/", "") + target = (self.itt or self.integration_test_target or self.test_target).replace( + "integration_tests/", "" + ) self._run( "Running pytest…", [ @@ -216,11 +247,12 @@ def run(self): setup( name="slackclient", version=__version__, - description="Slack API clients for Web API and RTM API", + description="Slack API clients for Web API and RTM API (Legacy) - " + + "Please use https://pypi.org/project/slack-sdk/ instead.", long_description=long_description, long_description_content_type="text/markdown", - url="https://github.com/slackapi/python-slackclient", - author="Slack Technologies, Inc.", + url="https://github.com/slackapi/python-slack-sdk", + author="Slack Technologies, LLC", author_email="opensource@slack.com", python_requires=">=3.6.0", include_package_data=True, @@ -237,14 +269,21 @@ def run(self): "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", ], keywords="slack slack-web slack-rtm chat chatbots bots chatops", packages=find_packages( - exclude=["docs", "docs-src", "integration_tests", "tests", "tests.*", "tutorial"] + exclude=[ + "docs", + "docs-src", + "integration_tests", + "tests", + "tests.*", + "tutorial", + ] ), - install_requires=[ - "aiohttp>3.5.2,<4.0.0" # TODO: move to extras_require in v3 - ], + install_requires=["aiohttp>3.5.2,<4.0.0"], # TODO: move to extras_require in v3 extras_require={"optional": ["aiodns>1.0"]}, setup_requires=pytest_runner, test_suite="tests", diff --git a/slack/rtm/client.py b/slack/rtm/client.py index 7968e5a0e..a3465f4be 100644 --- a/slack/rtm/client.py +++ b/slack/rtm/client.py @@ -569,7 +569,7 @@ async def _wait_exponentially(self, exception, max_wait_time=300): """ wait_time = exception.response.get("headers", {}).get( "Retry-After", - min((2 ** self._connection_attempts) + random.random(), max_wait_time), + min((2**self._connection_attempts) + random.random(), max_wait_time), ) self._logger.debug("Waiting %s seconds before reconnecting.", wait_time) await asyncio.sleep(float(wait_time)) @@ -580,7 +580,8 @@ def _close_websocket(self) -> List[Future]: close_method = getattr(self._websocket, "close", None) if callable(close_method): future = asyncio.ensure_future( - close_method(), loop=self._event_loop, # skipcq: PYL-E1102 + close_method(), + loop=self._event_loop, # skipcq: PYL-E1102 ) futures.append(future) self._websocket = None diff --git a/slack/signature/verifier.py b/slack/signature/verifier.py index e42ca6011..9217502b0 100644 --- a/slack/signature/verifier.py +++ b/slack/signature/verifier.py @@ -23,7 +23,9 @@ def __init__(self, signing_secret: str, clock: Clock = Clock()): self.clock = clock def is_valid_request( - self, body: Union[str, bytes], headers: Dict[str, str], + self, + body: Union[str, bytes], + headers: Dict[str, str], ) -> bool: """Verifies if the given signature is valid""" if headers is None: @@ -36,7 +38,10 @@ def is_valid_request( ) def is_valid( - self, body: Union[str, bytes], timestamp: str, signature: str, + self, + body: Union[str, bytes], + timestamp: str, + signature: str, ) -> bool: """Verifies if the given signature is valid""" if timestamp is None or signature is None: diff --git a/slack/version.py b/slack/version.py index d5546fe61..402791620 100644 --- a/slack/version.py +++ b/slack/version.py @@ -1,2 +1,2 @@ # see: http://legacy.python.org/dev/peps/pep-0440/#public-version-identifiers -__version__ = "2.9.3" +__version__ = "2.9.4" diff --git a/slack/web/async_base_client.py b/slack/web/async_base_client.py index 053c4fa46..e8efa0853 100644 --- a/slack/web/async_base_client.py +++ b/slack/web/async_base_client.py @@ -111,7 +111,9 @@ async def api_call( # skipcq: PYL-R1710 show_2020_01_deprecation(api_method) return await self._send( - http_verb=http_verb, api_url=api_url, req_args=req_args, + http_verb=http_verb, + api_url=api_url, + req_args=req_args, ) async def _send( diff --git a/slack/web/async_slack_response.py b/slack/web/async_slack_response.py index 462b7d2dc..83a3e4244 100644 --- a/slack/web/async_slack_response.py +++ b/slack/web/async_slack_response.py @@ -133,7 +133,9 @@ async def __anext__(self): self.req_args.update({"params": params}) response = await self._client._request( # skipcq: PYL-W0212 - http_verb=self.http_verb, api_url=self.api_url, req_args=self.req_args, + http_verb=self.http_verb, + api_url=self.api_url, + req_args=self.req_args, ) self.data = response["data"] diff --git a/slack/web/classes/blocks.py b/slack/web/classes/blocks.py index b05f80261..8bf55d3c9 100644 --- a/slack/web/classes/blocks.py +++ b/slack/web/classes/blocks.py @@ -159,7 +159,10 @@ class DividerBlock(Block): type = "divider" def __init__( - self, *, block_id: Optional[str] = None, **others: dict, + self, + *, + block_id: Optional[str] = None, + **others: dict, ): """A content divider, like an
, to split up different blocks inside of a message. https://api.slack.com/reference/block-kit/blocks#divider @@ -238,7 +241,7 @@ def __init__( super().__init__(type=self.type, block_id=block_id) show_unknown_key_warning(self, others) - self.elements = elements + self.elements = BlockElement.parse_all(elements) @JsonValidator(f"elements attribute cannot exceed {elements_max_length} elements") def _validate_elements_length(self): diff --git a/slack/web/classes/objects.py b/slack/web/classes/objects.py index 214deb885..fc5b39622 100644 --- a/slack/web/classes/objects.py +++ b/slack/web/classes/objects.py @@ -558,7 +558,9 @@ def parse(cls, config: Union["DispatchActionConfig", dict]): return None def __init__( - self, *, trigger_actions_on: Optional[list] = None, + self, + *, + trigger_actions_on: Optional[list] = None, ): """ Determines when a plain-text input element will return a block_actions interaction payload. diff --git a/slack/web/classes/views.py b/slack/web/classes/views.py index 39827e57c..1951a9cb9 100644 --- a/slack/web/classes/views.py +++ b/slack/web/classes/views.py @@ -152,7 +152,9 @@ def _show_warning_about_unknown(cls, value): ) def __init__( - self, *, values: Dict[str, Dict[str, Union[dict, "ViewStateValue"]]], + self, + *, + values: Dict[str, Dict[str, Union[dict, "ViewStateValue"]]], ): value_objects: Dict[str, Dict[str, ViewStateValue]] = {} new_state_values = copy.copy(values) diff --git a/slack/webhook/client.py b/slack/webhook/client.py index ac376ccc5..5a7d3a55f 100644 --- a/slack/webhook/client.py +++ b/slack/webhook/client.py @@ -147,7 +147,10 @@ def _perform_http_request( charset = e.headers.get_content_charset() or "utf-8" body: str = e.read().decode(charset) # read the response body here resp = WebhookResponse( - url=url, status_code=e.code, body=body, headers=e.headers, + url=url, + status_code=e.code, + body=body, + headers=e.headers, ) if e.code == 429: # for backward-compatibility with WebClient (v.2.5.0 or older) diff --git a/slack/webhook/internal_utils.py b/slack/webhook/internal_utils.py index 1fbb52f98..3cd6df132 100644 --- a/slack/webhook/internal_utils.py +++ b/slack/webhook/internal_utils.py @@ -14,7 +14,8 @@ def _build_body(original_body: Dict[str, any]) -> Dict[str, any]: def _build_request_headers( - default_headers: Dict[str, str], additional_headers: Optional[Dict[str, str]], + default_headers: Dict[str, str], + additional_headers: Optional[Dict[str, str]], ) -> Dict[str, str]: if default_headers is None and additional_headers is None: return {} diff --git a/slack/webhook/webhook_response.py b/slack/webhook/webhook_response.py index b2a016af9..5da6b8086 100644 --- a/slack/webhook/webhook_response.py +++ b/slack/webhook/webhook_response.py @@ -1,6 +1,11 @@ class WebhookResponse: def __init__( - self, *, url: str, status_code: int, body: str, headers: dict, + self, + *, + url: str, + status_code: int, + body: str, + headers: dict, ): self.api_url = url self.status_code = status_code diff --git a/tests/web/classes/test_blocks.py b/tests/web/classes/test_blocks.py index 8f69b649f..6b3e50d05 100644 --- a/tests/web/classes/test_blocks.py +++ b/tests/web/classes/test_blocks.py @@ -10,7 +10,15 @@ ImageBlock, SectionBlock, InputBlock, FileBlock, Block, CallBlock, ) -from slack.web.classes.elements import ButtonElement, ImageElement, LinkButtonElement +from slack.web.classes.elements import ( + ButtonElement, + ImageElement, + LinkButtonElement, + StaticSelectElement, + OverflowMenuElement, + Option, +) + from slack.web.classes.objects import PlainTextObject, MarkdownTextObject from . import STRING_3001_CHARS @@ -426,6 +434,28 @@ def test_json(self): with self.assertRaises(SlackObjectFormationError): ActionsBlock(elements=self.elements * 3).to_dict() + def test_element_parsing(self): + elements = [ + ButtonElement(text="Click me", action_id="reg_button", value="1"), + StaticSelectElement(options=[Option(value='SelectOption')]), + ImageElement(image_url='url', alt_text='alt-text'), + OverflowMenuElement(options=[Option(value='MenuOption1'), Option(value='MenuOption2')]), + ] + input = { + "type": "actions", + "block_id": "actionblock789", + "elements": [ + e.to_dict() for e in elements + ], + } + parsed_elements = ActionsBlock(**input).elements + self.assertEqual(len(elements), len(parsed_elements)) + for original, parsed in zip(elements, parsed_elements): + self.assertEqual(type(original), type(parsed)) + self.assertDictEqual(original.to_dict(), parsed.to_dict()) + + + # ---------------------------------------------- # Context @@ -743,4 +773,3 @@ def test_text_length_151(self): } with self.assertRaises(SlackObjectFormationError): HeaderBlock(**input).validate_json() -