csp-adapter-slack 0.1.0__tar.gz → 0.2.0__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.
- {csp_adapter_slack-0.1.0 → csp_adapter_slack-0.2.0}/PKG-INFO +8 -6
- csp_adapter_slack-0.2.0/csp_adapter_slack/__init__.py +4 -0
- {csp_adapter_slack-0.1.0 → csp_adapter_slack-0.2.0}/csp_adapter_slack/adapter.py +12 -23
- csp_adapter_slack-0.2.0/csp_adapter_slack/adapter_config.py +37 -0
- {csp_adapter_slack-0.1.0 → csp_adapter_slack-0.2.0}/csp_adapter_slack/tests/test_adapter.py +9 -5
- {csp_adapter_slack-0.1.0 → csp_adapter_slack-0.2.0}/pyproject.toml +47 -16
- csp_adapter_slack-0.1.0/csp_adapter_slack/__init__.py +0 -3
- {csp_adapter_slack-0.1.0 → csp_adapter_slack-0.2.0}/.gitignore +0 -0
- {csp_adapter_slack-0.1.0 → csp_adapter_slack-0.2.0}/LICENSE +0 -0
- {csp_adapter_slack-0.1.0 → csp_adapter_slack-0.2.0}/README.md +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.3
|
2
2
|
Name: csp_adapter_slack
|
3
|
-
Version: 0.
|
3
|
+
Version: 0.2.0
|
4
4
|
Summary: A csp adapter for slack
|
5
5
|
Project-URL: Repository, https://github.com/point72/csp-adapter-slack
|
6
6
|
Project-URL: Homepage, https://github.com/point72/csp-adapter-slack
|
@@ -206,7 +206,6 @@ License: Apache License
|
|
206
206
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
207
207
|
See the License for the specific language governing permissions and
|
208
208
|
limitations under the License.
|
209
|
-
License-File: LICENSE
|
210
209
|
Keywords: chat,chatbot,csp,slack,stream-processing
|
211
210
|
Classifier: Development Status :: 4 - Beta
|
212
211
|
Classifier: Framework :: Jupyter
|
@@ -218,19 +217,22 @@ Classifier: Programming Language :: Python :: 3.9
|
|
218
217
|
Classifier: Programming Language :: Python :: 3.10
|
219
218
|
Classifier: Programming Language :: Python :: 3.11
|
220
219
|
Classifier: Programming Language :: Python :: 3.12
|
220
|
+
Classifier: Programming Language :: Python :: 3.13
|
221
221
|
Requires-Python: >=3.8
|
222
222
|
Requires-Dist: csp
|
223
|
+
Requires-Dist: pydantic>=2
|
223
224
|
Requires-Dist: slack-sdk>=3
|
224
225
|
Provides-Extra: develop
|
225
|
-
Requires-Dist:
|
226
|
+
Requires-Dist: bump-my-version; extra == 'develop'
|
226
227
|
Requires-Dist: check-manifest; extra == 'develop'
|
227
|
-
Requires-Dist: codespell<2.
|
228
|
+
Requires-Dist: codespell<2.4,>=2.2.6; extra == 'develop'
|
228
229
|
Requires-Dist: hatchling; extra == 'develop'
|
230
|
+
Requires-Dist: mdformat-tables<1.1,>=1; extra == 'develop'
|
229
231
|
Requires-Dist: mdformat<0.8,>=0.7.17; extra == 'develop'
|
230
232
|
Requires-Dist: pytest; extra == 'develop'
|
231
233
|
Requires-Dist: pytest-cov; extra == 'develop'
|
232
|
-
Requires-Dist: ruff<0.
|
233
|
-
Requires-Dist: twine<
|
234
|
+
Requires-Dist: ruff<0.9,>=0.5; extra == 'develop'
|
235
|
+
Requires-Dist: twine<7,>=5; extra == 'develop'
|
234
236
|
Provides-Extra: test
|
235
237
|
Requires-Dist: pytest; extra == 'test'
|
236
238
|
Requires-Dist: pytest-cov; extra == 'test'
|
@@ -1,10 +1,9 @@
|
|
1
1
|
import threading
|
2
2
|
from logging import getLogger
|
3
3
|
from queue import Queue
|
4
|
-
from ssl import SSLContext
|
5
4
|
from threading import Thread
|
6
5
|
from time import sleep
|
7
|
-
from typing import Dict, List,
|
6
|
+
from typing import Dict, List, TypeVar
|
8
7
|
|
9
8
|
import csp
|
10
9
|
from csp.impl.adaptermanager import AdapterManagerImpl
|
@@ -13,13 +12,14 @@ from csp.impl.pushadapter import PushInputAdapter
|
|
13
12
|
from csp.impl.struct import Struct
|
14
13
|
from csp.impl.types.tstype import ts
|
15
14
|
from csp.impl.wiring import py_output_adapter_def, py_push_adapter_def
|
16
|
-
|
17
15
|
from slack_sdk.errors import SlackApiError
|
18
16
|
from slack_sdk.socket_mode import SocketModeClient
|
19
17
|
from slack_sdk.socket_mode.request import SocketModeRequest
|
20
18
|
from slack_sdk.socket_mode.response import SocketModeResponse
|
21
19
|
from slack_sdk.web import WebClient
|
22
20
|
|
21
|
+
from .adapter_config import SlackAdapterConfig
|
22
|
+
|
23
23
|
T = TypeVar("T")
|
24
24
|
log = getLogger(__file__)
|
25
25
|
|
@@ -49,13 +49,10 @@ def mention_user(userid: str) -> str:
|
|
49
49
|
|
50
50
|
|
51
51
|
class SlackAdapterManager(AdapterManagerImpl):
|
52
|
-
def __init__(self,
|
53
|
-
if not app_token.startswith("xapp-") or not bot_token.startswith("xoxb-"):
|
54
|
-
raise RuntimeError("Slack app token or bot token looks malformed")
|
55
|
-
|
52
|
+
def __init__(self, config: SlackAdapterConfig):
|
56
53
|
self._slack_client = SocketModeClient(
|
57
|
-
app_token=app_token,
|
58
|
-
web_client=WebClient(token=bot_token, ssl=ssl),
|
54
|
+
app_token=config.app_token,
|
55
|
+
web_client=WebClient(token=config.bot_token, ssl=config.ssl),
|
59
56
|
)
|
60
57
|
self._slack_client.socket_mode_request_listeners.append(self._process_slack_message)
|
61
58
|
|
@@ -121,7 +118,7 @@ class SlackAdapterManager(AdapterManagerImpl):
|
|
121
118
|
if ret.status_code == 200:
|
122
119
|
# TODO OAuth scopes required
|
123
120
|
name = ret.data["user"]["profile"].get("real_name_normalized", ret.data["user"]["name"])
|
124
|
-
email = ret.data["user"]["profile"]
|
121
|
+
email = ret.data["user"]["profile"].get("email", "")
|
125
122
|
self._user_id_to_user_name[user_id] = name
|
126
123
|
self._user_name_to_user_id[name] = user_id # TODO is this 1-1 in slack?
|
127
124
|
self._user_id_to_user_email[user_id] = email
|
@@ -200,14 +197,8 @@ class SlackAdapterManager(AdapterManagerImpl):
|
|
200
197
|
return self._room_name_to_room_id.get(channel_name, None)
|
201
198
|
return channel_id
|
202
199
|
|
203
|
-
def _get_tags_from_message(self, blocks
|
200
|
+
def _get_tags_from_message(self, blocks) -> List[str]:
|
204
201
|
"""extract tags from message, potentially excluding the bot's own @"""
|
205
|
-
authorizations = authorizations or []
|
206
|
-
if len(authorizations) > 0:
|
207
|
-
bot_id = authorizations[0]["user_id"] # TODO more than one?
|
208
|
-
else:
|
209
|
-
bot_id = ""
|
210
|
-
|
211
202
|
tags = []
|
212
203
|
to_search = blocks.copy()
|
213
204
|
|
@@ -219,11 +210,9 @@ class SlackAdapterManager(AdapterManagerImpl):
|
|
219
210
|
|
220
211
|
if element.get("type", "") == "user":
|
221
212
|
tag_id = element.get("user_id")
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
if name:
|
226
|
-
tags.append(name)
|
213
|
+
name, _ = self._get_user_from_id(tag_id)
|
214
|
+
if name:
|
215
|
+
tags.append(name)
|
227
216
|
return tags
|
228
217
|
|
229
218
|
def _process_slack_message(self, client: SocketModeClient, req: SocketModeRequest):
|
@@ -236,7 +225,7 @@ class SlackAdapterManager(AdapterManagerImpl):
|
|
236
225
|
if req.payload["event"]["type"] in ("message", "app_mention") and req.payload["event"].get("subtype") is None:
|
237
226
|
user, user_email = self._get_user_from_id(req.payload["event"]["user"])
|
238
227
|
channel, channel_type = self._get_channel_from_id(req.payload["event"]["channel"])
|
239
|
-
tags = self._get_tags_from_message(req.payload["event"]["blocks"]
|
228
|
+
tags = self._get_tags_from_message(req.payload["event"]["blocks"])
|
240
229
|
slack_msg = SlackMessage(
|
241
230
|
user=user or "",
|
242
231
|
user_email=user_email or "",
|
@@ -0,0 +1,37 @@
|
|
1
|
+
from pathlib import Path
|
2
|
+
from ssl import SSLContext
|
3
|
+
from typing import Optional
|
4
|
+
|
5
|
+
from pydantic import BaseModel, Field, field_validator
|
6
|
+
|
7
|
+
__all__ = ("SlackAdapterConfig",)
|
8
|
+
|
9
|
+
|
10
|
+
class SlackAdapterConfig(BaseModel):
|
11
|
+
"""A config class that holds the required information to interact with Slack."""
|
12
|
+
|
13
|
+
app_token: str = Field(description="The app token for the Slack bot")
|
14
|
+
bot_token: str = Field(description="The bot token for the Slack bot")
|
15
|
+
ssl: Optional[object] = None
|
16
|
+
|
17
|
+
@field_validator("app_token")
|
18
|
+
def validate_app_token(cls, v):
|
19
|
+
if v.startswith("xapp-"):
|
20
|
+
return v
|
21
|
+
elif Path(v).exists():
|
22
|
+
return Path(v).read_text().strip()
|
23
|
+
raise ValueError("App token must start with 'xoxb-' or be a file path")
|
24
|
+
|
25
|
+
@field_validator("bot_token")
|
26
|
+
def validate_bot_token(cls, v):
|
27
|
+
if v.startswith("xoxb-"):
|
28
|
+
return v
|
29
|
+
elif Path(v).exists():
|
30
|
+
return Path(v).read_text().strip()
|
31
|
+
raise ValueError("Bot token must start with 'xoxb-' or be a file path")
|
32
|
+
|
33
|
+
@field_validator("ssl")
|
34
|
+
def validate_ssl(cls, v):
|
35
|
+
# TODO pydantic validation via schema
|
36
|
+
assert v is None or isinstance(v, SSLContext)
|
37
|
+
return v
|
@@ -1,11 +1,13 @@
|
|
1
|
-
import pytest
|
2
1
|
from datetime import timedelta
|
3
2
|
from ssl import create_default_context
|
4
3
|
from unittest.mock import MagicMock, call, patch
|
5
4
|
|
6
5
|
import csp
|
6
|
+
import pytest
|
7
7
|
from csp import ts
|
8
|
-
from
|
8
|
+
from pydantic import ValidationError
|
9
|
+
|
10
|
+
from csp_adapter_slack import SlackAdapterConfig, SlackAdapterManager, SlackMessage, mention_user
|
9
11
|
|
10
12
|
|
11
13
|
@csp.node
|
@@ -119,8 +121,10 @@ DIRECT_MESSAGE_PAYLOAD = {
|
|
119
121
|
|
120
122
|
class TestSlack:
|
121
123
|
def test_slack_tokens(self):
|
122
|
-
with pytest.raises(
|
123
|
-
|
124
|
+
with pytest.raises(ValidationError):
|
125
|
+
SlackAdapterConfig(app_token="abc", bot_token="xoxb-def")
|
126
|
+
with pytest.raises(ValidationError):
|
127
|
+
SlackAdapterConfig(app_token="xapp-abc", bot_token="def")
|
124
128
|
|
125
129
|
@pytest.mark.parametrize("payload", (PUBLIC_CHANNEL_MENTION_PAYLOAD, DIRECT_MESSAGE_PAYLOAD))
|
126
130
|
def test_slack(self, payload):
|
@@ -150,7 +154,7 @@ class TestSlack:
|
|
150
154
|
clientmock.return_value.web_client.conversations_list.return_value = mock_list_response
|
151
155
|
|
152
156
|
def graph():
|
153
|
-
am = SlackAdapterManager("xapp-1-dummy", "xoxb-dummy", ssl=create_default_context())
|
157
|
+
am = SlackAdapterManager(SlackAdapterConfig(app_token="xapp-1-dummy", bot_token="xoxb-dummy", ssl=create_default_context()))
|
154
158
|
|
155
159
|
# send a fake slack message to the app
|
156
160
|
stop = send_fake_message(clientmock, reqmock, am)
|
@@ -1,14 +1,14 @@
|
|
1
1
|
[build-system]
|
2
2
|
requires = [
|
3
|
-
"hatchling>=1.22.4,<1.
|
4
|
-
"pkginfo>=1.10,<1.
|
3
|
+
"hatchling>=1.22.4,<1.27",
|
4
|
+
"pkginfo>=1.10,<1.12",
|
5
5
|
]
|
6
6
|
build-backend = "hatchling.build"
|
7
7
|
|
8
8
|
[project]
|
9
9
|
name = "csp_adapter_slack"
|
10
10
|
description = "A csp adapter for slack"
|
11
|
-
version = "0.
|
11
|
+
version = "0.2.0"
|
12
12
|
readme = "README.md"
|
13
13
|
license = { file = "LICENSE" }
|
14
14
|
requires-python = ">=3.8"
|
@@ -30,22 +30,25 @@ classifiers = [
|
|
30
30
|
"Programming Language :: Python :: 3.10",
|
31
31
|
"Programming Language :: Python :: 3.11",
|
32
32
|
"Programming Language :: Python :: 3.12",
|
33
|
+
"Programming Language :: Python :: 3.13",
|
33
34
|
"License :: OSI Approved :: Apache Software License",
|
34
35
|
]
|
35
36
|
dependencies = [
|
36
37
|
"csp",
|
38
|
+
"pydantic>=2",
|
37
39
|
"slack-sdk>=3",
|
38
40
|
]
|
39
41
|
|
40
42
|
[project.optional-dependencies]
|
41
43
|
develop = [
|
42
|
-
"
|
44
|
+
"bump-my-version",
|
43
45
|
"check-manifest",
|
44
|
-
"codespell>=2.2.6,<2.
|
46
|
+
"codespell>=2.2.6,<2.4",
|
45
47
|
"hatchling",
|
46
48
|
"mdformat>=0.7.17,<0.8",
|
47
|
-
"
|
48
|
-
"
|
49
|
+
"mdformat-tables>=1,<1.1",
|
50
|
+
"ruff>=0.5,<0.9",
|
51
|
+
"twine>=5,<7",
|
49
52
|
# test
|
50
53
|
"pytest",
|
51
54
|
"pytest-cov",
|
@@ -59,9 +62,38 @@ test = [
|
|
59
62
|
Repository = "https://github.com/point72/csp-adapter-slack"
|
60
63
|
Homepage = "https://github.com/point72/csp-adapter-slack"
|
61
64
|
|
65
|
+
[tool.bumpversion]
|
66
|
+
current_version = "0.2.0"
|
67
|
+
commit = true
|
68
|
+
tag = false
|
69
|
+
commit_args = "-s"
|
70
|
+
|
71
|
+
[[tool.bumpversion.files]]
|
72
|
+
filename = "csp_adapter_slack/__init__.py"
|
73
|
+
search = '__version__ = "{current_version}"'
|
74
|
+
replace = '__version__ = "{new_version}"'
|
75
|
+
|
76
|
+
[[tool.bumpversion.files]]
|
77
|
+
filename = "pyproject.toml"
|
78
|
+
search = 'version = "{current_version}"'
|
79
|
+
replace = 'version = "{new_version}"'
|
80
|
+
|
62
81
|
[tool.check-manifest]
|
63
82
|
ignore = []
|
64
83
|
|
84
|
+
[tool.coverage.run]
|
85
|
+
branch = true
|
86
|
+
omit = []
|
87
|
+
|
88
|
+
[tool.coverage.report]
|
89
|
+
exclude_also = [
|
90
|
+
"raise NotImplementedError",
|
91
|
+
"if __name__ == .__main__.:",
|
92
|
+
"@(abc\\.)?abstractmethod",
|
93
|
+
]
|
94
|
+
ignore_errors = true
|
95
|
+
fail_under = 75
|
96
|
+
|
65
97
|
[tool.hatch.build]
|
66
98
|
artifacts = []
|
67
99
|
|
@@ -69,11 +101,7 @@ artifacts = []
|
|
69
101
|
src = "/"
|
70
102
|
|
71
103
|
[tool.hatch.build.targets.sdist]
|
72
|
-
|
73
|
-
"/csp_adapter_slack",
|
74
|
-
"LICENSE",
|
75
|
-
"README.md",
|
76
|
-
]
|
104
|
+
packages = ["csp_adapter_slack"]
|
77
105
|
exclude = [
|
78
106
|
"/.github",
|
79
107
|
"/.gitignore",
|
@@ -81,9 +109,7 @@ exclude = [
|
|
81
109
|
]
|
82
110
|
|
83
111
|
[tool.hatch.build.targets.wheel]
|
84
|
-
|
85
|
-
"/csp_adapter_slack",
|
86
|
-
]
|
112
|
+
packages = ["csp_adapter_slack"]
|
87
113
|
exclude = [
|
88
114
|
"/.github",
|
89
115
|
"/.gitignore",
|
@@ -92,12 +118,16 @@ exclude = [
|
|
92
118
|
]
|
93
119
|
|
94
120
|
[tool.pytest.ini_options]
|
121
|
+
addopts = ["-vvv", "--junitxml=junit.xml"]
|
95
122
|
asyncio_mode = "strict"
|
96
123
|
testpaths = "csp_adapter_slack/tests"
|
97
124
|
|
98
125
|
[tool.ruff]
|
99
126
|
line-length = 150
|
100
127
|
|
128
|
+
[tool.ruff.lint]
|
129
|
+
extend-select = ["I"]
|
130
|
+
|
101
131
|
[tool.ruff.lint.per-file-ignores]
|
102
132
|
"__init__.py" = ["F401", "F403"]
|
103
133
|
|
@@ -107,7 +137,8 @@ default-section = "third-party"
|
|
107
137
|
known-first-party = ["csp_adapter_slack"]
|
108
138
|
section-order = [
|
109
139
|
"future",
|
140
|
+
"standard-library",
|
110
141
|
"third-party",
|
111
142
|
"first-party",
|
112
143
|
"local-folder",
|
113
|
-
]
|
144
|
+
]
|
File without changes
|
File without changes
|
File without changes
|