slack-remote 0.0.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,22 @@
1
+ Metadata-Version: 2.4
2
+ Name: slack-remote
3
+ Version: 0.0.1
4
+ Summary: PyPI slack-remote Python Package owned by Circlez.ai
5
+ Home-page: https://github.com/circles-zone/slack-remote-python-package
6
+ Author: Circles
7
+ Author-email: info@circlez.ai
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: Operating System :: OS Independent
10
+ Description-Content-Type: text/markdown
11
+ Requires-Dist: python-sdk-remote
12
+ Requires-Dist: requests
13
+ Dynamic: author
14
+ Dynamic: author-email
15
+ Dynamic: classifier
16
+ Dynamic: description
17
+ Dynamic: description-content-type
18
+ Dynamic: home-page
19
+ Dynamic: requires-dist
20
+ Dynamic: summary
21
+
22
+ Slack Web API and Events API: post a message, read the replies in its thread, send a signed event
@@ -0,0 +1,34 @@
1
+ # slack-remote-python-package
2
+
3
+ PyPI [slack-remote](https://pypi.org/project/slack-remote/): the Slack Web API with a bot token, and Events API
4
+ requests signed as Slack signs them - for packages that talk to Slack and for the Sanity/Smoke Tests of our Slack
5
+ apps (i.e. the play1 Slack webhook in `#request-to-open-a-jira-work-item-play1`, never in a channel of real users).
6
+
7
+ ```python
8
+ from slack_remote.src import SlackRemote
9
+
10
+ slack_remote = SlackRemote() # SLACK_BOT_TOKEN
11
+ message = slack_remote.post_message(channel_id, "Summary: ...")
12
+ thread_reply = slack_remote.wait_for_thread_reply(channel_id, message["ts"])
13
+
14
+ # Smoke test of a Request URL: the event Slack would send for a message, signed with the app's Signing Secret
15
+ response = SlackRemote.send_signed_event(request_url, signing_secret, {"type": "message", "channel": channel_id,
16
+ "user": user_id, "text": "...", "ts": ts})
17
+ ```
18
+
19
+ | Method | Bot scopes |
20
+ |---|---|
21
+ | `post_message` | chat:write |
22
+ | `get_thread_replies`, `wait_for_thread_reply` | channels:history (groups:history in a private channel) |
23
+ | `get_user_email` | users:read, users:read.email |
24
+
25
+ ## Environment variables
26
+ - `SLACK_BOT_TOKEN` (or `SlackRemote(bot_token=...)`)
27
+ - System tests: also `SLACK_TEST_CHANNEL_ID` of a test channel the bot is a member of; without them they are skipped.
28
+
29
+ ## Tests
30
+ ```shell
31
+ cd slack-remote-python-package
32
+ pip install -r requirements.txt -r requirements-dev.txt
33
+ pytest
34
+ ```
@@ -0,0 +1,12 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61.0"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [tool.poetry]
6
+ name = "slack-remote"
7
+ version = "0.0.1" # https://pypi.org/project/slack-remote
8
+ description = "slack-remote Python Package"
9
+ readme = "README.md"
10
+ authors = [
11
+ "Circlez.ai <info@circlez.ai>",
12
+ ]
@@ -0,0 +1,4 @@
1
+ from .slack_remote import SlackRemote
2
+ from .slack_remote_exception import SlackRemoteException
3
+
4
+ __all__ = ["SlackRemote", "SlackRemoteException"]
@@ -0,0 +1,7 @@
1
+ SLACK_API_BASE_URL = "https://slack.com/api"
2
+ SLACK_BOT_TOKEN_ENV = "SLACK_BOT_TOKEN"
3
+ SLACK_UNAVAILABLE_ERROR_CODE = "slack_unavailable"
4
+ HTTP_REQUEST_TIMEOUT_SECONDS = 15
5
+ # A bot usually replies in the thread within ~10 seconds
6
+ WAIT_FOR_THREAD_REPLY_TIMEOUT_SECONDS = 60
7
+ WAIT_FOR_THREAD_REPLY_POLL_SECONDS = 3
@@ -0,0 +1,93 @@
1
+ import hashlib
2
+ import hmac
3
+ import json
4
+ import time
5
+ from http import HTTPMethod
6
+ from typing import Any, Callable, Optional
7
+
8
+ import requests
9
+ from python_sdk_remote.utilities import our_get_env
10
+
11
+ from .constants_slack_remote import (
12
+ HTTP_REQUEST_TIMEOUT_SECONDS,
13
+ SLACK_API_BASE_URL,
14
+ SLACK_BOT_TOKEN_ENV,
15
+ SLACK_UNAVAILABLE_ERROR_CODE,
16
+ WAIT_FOR_THREAD_REPLY_POLL_SECONDS,
17
+ WAIT_FOR_THREAD_REPLY_TIMEOUT_SECONDS,
18
+ )
19
+ from .slack_remote_exception import SlackRemoteException
20
+
21
+
22
+ class SlackRemote:
23
+ """Slack Web API with a bot token, and Events API requests signed as Slack signs them (for smoke tests of a
24
+ Request URL)."""
25
+
26
+ def __init__(self, bot_token: Optional[str] = None) -> None:
27
+ self.bot_token = bot_token or our_get_env(SLACK_BOT_TOKEN_ENV, raise_if_empty=True)
28
+
29
+ def post_message(self, channel: str, text: str, thread_ts: Optional[str] = None) -> dict[str, Any]:
30
+ """Needs the chat:write bot scope. In Slack's response "channel" is the channel id and "ts" the timestamp of
31
+ the message (the thread_ts of its replies)."""
32
+ message: dict[str, Any] = {"channel": channel, "text": text}
33
+ if thread_ts:
34
+ message["thread_ts"] = thread_ts
35
+ return self._call(HTTPMethod.POST, "chat.postMessage", json=message)
36
+
37
+ def get_thread_replies(self, channel: str, thread_ts: str) -> list[dict[str, Any]]:
38
+ """Needs the channels:history (groups:history in a private channel) bot scope. Only the replies, without the
39
+ message that started the thread."""
40
+ response_json = self._call(HTTPMethod.GET, "conversations.replies",
41
+ params={"channel": channel, "ts": thread_ts})
42
+ thread_replies = [message for message in response_json.get("messages", []) if message.get("ts") != thread_ts]
43
+ return thread_replies
44
+
45
+ def wait_for_thread_reply(self, channel: str, thread_ts: str,
46
+ timeout_seconds: float = WAIT_FOR_THREAD_REPLY_TIMEOUT_SECONDS,
47
+ poll_seconds: float = WAIT_FOR_THREAD_REPLY_POLL_SECONDS,
48
+ sleep: Callable[[float], None] = time.sleep) -> Optional[dict[str, Any]]:
49
+ """The first reply in the thread, or None when no one replied within timeout_seconds."""
50
+ thread_reply = None
51
+ waited_seconds = 0.0
52
+ while thread_reply is None and waited_seconds <= timeout_seconds:
53
+ thread_replies = self.get_thread_replies(channel, thread_ts)
54
+ if thread_replies:
55
+ thread_reply = thread_replies[0]
56
+ else:
57
+ sleep(poll_seconds)
58
+ waited_seconds += poll_seconds
59
+ return thread_reply
60
+
61
+ def get_user_email(self, user_id: str) -> Optional[str]:
62
+ """Needs the users:read and users:read.email bot scopes."""
63
+ response_json = self._call(HTTPMethod.GET, "users.info", params={"user": user_id})
64
+ user_email: Optional[str] = (response_json.get("user") or {}).get("profile", {}).get("email")
65
+ return user_email
66
+
67
+ @staticmethod
68
+ def send_signed_event(request_url: str, signing_secret: str, event: dict[str, Any],
69
+ timestamp: Optional[int] = None) -> requests.Response:
70
+ """POSTs an Events API event_callback to a Request URL with the signature Slack sends
71
+ (https://api.slack.com/authentication/verifying-requests-from-slack)."""
72
+ body = json.dumps({"type": "event_callback", "event": event})
73
+ request_timestamp = str(timestamp if timestamp is not None else int(time.time()))
74
+ signature = "v0=" + hmac.new(signing_secret.encode(), f"v0:{request_timestamp}:{body}".encode(),
75
+ hashlib.sha256).hexdigest()
76
+ response = requests.post(request_url, data=body.encode(), timeout=HTTP_REQUEST_TIMEOUT_SECONDS,
77
+ headers={"Content-Type": "application/json",
78
+ "X-Slack-Request-Timestamp": request_timestamp,
79
+ "X-Slack-Signature": signature})
80
+ return response
81
+
82
+ def _call(self, method: HTTPMethod, api_method: str, **kwargs: Any) -> dict[str, Any]:
83
+ url = f"{SLACK_API_BASE_URL}/{api_method}"
84
+ try:
85
+ response_json: dict[str, Any] = requests.request(
86
+ method.value, url, headers={"Authorization": f"Bearer {self.bot_token}"},
87
+ timeout=HTTP_REQUEST_TIMEOUT_SECONDS, **kwargs).json()
88
+ except (requests.RequestException, ValueError) as exception:
89
+ raise SlackRemoteException(SLACK_UNAVAILABLE_ERROR_CODE, f"Slack {api_method} failed: {exception}")
90
+ if not response_json.get("ok"):
91
+ error_code = response_json.get("error") or "unknown_error"
92
+ raise SlackRemoteException(error_code, f"Slack {api_method} failed: {error_code}")
93
+ return response_json
@@ -0,0 +1,6 @@
1
+ class SlackRemoteException(Exception):
2
+ """error_code is the error of Slack (i.e. channel_not_found, missing_scope) or slack_unavailable."""
3
+
4
+ def __init__(self, error_code: str, message: str) -> None:
5
+ super().__init__(message)
6
+ self.error_code = error_code
@@ -0,0 +1,22 @@
1
+ Metadata-Version: 2.4
2
+ Name: slack-remote
3
+ Version: 0.0.1
4
+ Summary: PyPI slack-remote Python Package owned by Circlez.ai
5
+ Home-page: https://github.com/circles-zone/slack-remote-python-package
6
+ Author: Circles
7
+ Author-email: info@circlez.ai
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: Operating System :: OS Independent
10
+ Description-Content-Type: text/markdown
11
+ Requires-Dist: python-sdk-remote
12
+ Requires-Dist: requests
13
+ Dynamic: author
14
+ Dynamic: author-email
15
+ Dynamic: classifier
16
+ Dynamic: description
17
+ Dynamic: description-content-type
18
+ Dynamic: home-page
19
+ Dynamic: requires-dist
20
+ Dynamic: summary
21
+
22
+ Slack Web API and Events API: post a message, read the replies in its thread, send a signed event
@@ -0,0 +1,13 @@
1
+ README.md
2
+ pyproject.toml
3
+ setup.py
4
+ python_packages/slack_remote/__init__.py
5
+ python_packages/slack_remote.egg-info/PKG-INFO
6
+ python_packages/slack_remote.egg-info/SOURCES.txt
7
+ python_packages/slack_remote.egg-info/dependency_links.txt
8
+ python_packages/slack_remote.egg-info/requires.txt
9
+ python_packages/slack_remote.egg-info/top_level.txt
10
+ python_packages/slack_remote/src/__init__.py
11
+ python_packages/slack_remote/src/constants_slack_remote.py
12
+ python_packages/slack_remote/src/slack_remote.py
13
+ python_packages/slack_remote/src/slack_remote_exception.py
@@ -0,0 +1,2 @@
1
+ python-sdk-remote
2
+ requests
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,24 @@
1
+ import setuptools
2
+
3
+ PACKAGE_NAME = "slack-remote"
4
+
5
+ setuptools.setup(
6
+ name=PACKAGE_NAME,
7
+ version='0.0.1', # https://pypi.org/project/slack-remote#history
8
+ author="Circles",
9
+ author_email="info@circlez.ai",
10
+ description=f"PyPI {PACKAGE_NAME} Python Package owned by Circlez.ai",
11
+ long_description="Slack Web API and Events API: post a message, read the replies in its thread, send a signed event",
12
+ long_description_content_type='text/markdown',
13
+ url=f"https://github.com/circles-zone/{PACKAGE_NAME}-python-package",
14
+ packages=setuptools.find_packages(where="python_packages", exclude=["*.tests", "*.tests.*"]),
15
+ package_dir={"": "python_packages"},
16
+ classifiers=[
17
+ "Programming Language :: Python :: 3",
18
+ "Operating System :: OS Independent",
19
+ ],
20
+ install_requires=[
21
+ 'python-sdk-remote',
22
+ 'requests',
23
+ ]
24
+ )