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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: csp_adapter_slack
3
- Version: 0.1.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: bump2version>=1.0.0; extra == 'develop'
226
+ Requires-Dist: bump-my-version; extra == 'develop'
226
227
  Requires-Dist: check-manifest; extra == 'develop'
227
- Requires-Dist: codespell<2.3,>=2.2.6; extra == 'develop'
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.6,>=0.5; extra == 'develop'
233
- Requires-Dist: twine<5.2,>=5; extra == 'develop'
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'
@@ -0,0 +1,4 @@
1
+ __version__ = "0.2.0"
2
+
3
+ from .adapter import *
4
+ from .adapter_config import *
@@ -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, Optional, TypeVar
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, app_token: str, bot_token: str, ssl: Optional[SSLContext] = None):
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"]["email"]
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, authorizations=None) -> List[str]:
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
- if tag_id != bot_id:
223
- # TODO tag with id or with name?
224
- name, _ = self._get_user_from_id(tag_id)
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"], req.payload["authorizations"])
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 csp_adapter_slack import SlackAdapterManager, SlackMessage, mention_user
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(RuntimeError):
123
- SlackAdapterManager("abc", "def")
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.23",
4
- "pkginfo>=1.10,<1.11",
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.1.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
- "bump2version>=1.0.0",
44
+ "bump-my-version",
43
45
  "check-manifest",
44
- "codespell>=2.2.6,<2.3",
46
+ "codespell>=2.2.6,<2.4",
45
47
  "hatchling",
46
48
  "mdformat>=0.7.17,<0.8",
47
- "ruff>=0.5,<0.6",
48
- "twine>=5,<5.2",
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
- include = [
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
- include = [
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
+ ]
@@ -1,3 +0,0 @@
1
- __version__ = "0.1.0"
2
-
3
- from .adapter import *