csp-adapter-slack 0.1.0__tar.gz → 0.3.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.
@@ -150,3 +150,7 @@ $RECYCLE.BIN/
150
150
  # Coverage data
151
151
  # -------------
152
152
  **/coverage/
153
+
154
+ # Secrets
155
+ .app_token
156
+ .bot_token
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.3
1
+ Metadata-Version: 2.4
2
2
  Name: csp_adapter_slack
3
- Version: 0.1.0
3
+ Version: 0.3.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
@@ -213,24 +213,26 @@ Classifier: Framework :: Jupyter
213
213
  Classifier: License :: OSI Approved :: Apache Software License
214
214
  Classifier: Programming Language :: Python
215
215
  Classifier: Programming Language :: Python :: 3
216
- Classifier: Programming Language :: Python :: 3.8
217
216
  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
221
- Requires-Python: >=3.8
220
+ Classifier: Programming Language :: Python :: 3.13
221
+ Requires-Python: >=3.9
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.5,>=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.12,>=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'
@@ -247,8 +249,14 @@ A [csp](https://github.com/point72/csp) adapter for [slack](https://slack.com)
247
249
 
248
250
  ## Features
249
251
 
252
+ The Slack adapter allows for reading and writing of messages from the [Slack](https://slack.com/) message platform using the [`slack-sdk`](https://tools.slack.dev/python-slack-sdk/).
253
+
250
254
  [More information is available in our wiki](https://github.com/Point72/csp-adapter-slack/wiki)
251
255
 
256
+ ## Chat Framework
257
+
258
+ [`csp-bot`](https://github.com/Point72/csp-bot) is a framework for writing cross-platform, command oriented chat bots.
259
+
252
260
  ## Installation
253
261
 
254
262
  Install with `pip`:
@@ -265,4 +273,4 @@ conda install csp csp-adapter-slack -c conda-forge
265
273
 
266
274
  ## License
267
275
 
268
- This software is licensed under the Apache 2.0 license. See the [LICENSE](LICENSE) file for details.
276
+ This software is licensed under the Apache 2.0 license. See the [LICENSE](https://github.com/Point72/csp-adapter-slack/blob/main/LICENSE) file for details.
@@ -9,8 +9,14 @@ A [csp](https://github.com/point72/csp) adapter for [slack](https://slack.com)
9
9
 
10
10
  ## Features
11
11
 
12
+ The Slack adapter allows for reading and writing of messages from the [Slack](https://slack.com/) message platform using the [`slack-sdk`](https://tools.slack.dev/python-slack-sdk/).
13
+
12
14
  [More information is available in our wiki](https://github.com/Point72/csp-adapter-slack/wiki)
13
15
 
16
+ ## Chat Framework
17
+
18
+ [`csp-bot`](https://github.com/Point72/csp-bot) is a framework for writing cross-platform, command oriented chat bots.
19
+
14
20
  ## Installation
15
21
 
16
22
  Install with `pip`:
@@ -27,4 +33,4 @@ conda install csp csp-adapter-slack -c conda-forge
27
33
 
28
34
  ## License
29
35
 
30
- This software is licensed under the Apache 2.0 license. See the [LICENSE](LICENSE) file for details.
36
+ This software is licensed under the Apache 2.0 license. See the [LICENSE](https://github.com/Point72/csp-adapter-slack/blob/main/LICENSE) file for details.
@@ -0,0 +1,6 @@
1
+ __version__ = "0.3.0"
2
+
3
+ from .adapter import *
4
+ from .adapter_config import *
5
+ from .mention import *
6
+ from .message import *
@@ -1,61 +1,37 @@
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
11
10
  from csp.impl.outputadapter import OutputAdapter
12
11
  from csp.impl.pushadapter import PushInputAdapter
13
- from csp.impl.struct import Struct
14
12
  from csp.impl.types.tstype import ts
15
13
  from csp.impl.wiring import py_output_adapter_def, py_push_adapter_def
16
-
17
14
  from slack_sdk.errors import SlackApiError
18
15
  from slack_sdk.socket_mode import SocketModeClient
19
16
  from slack_sdk.socket_mode.request import SocketModeRequest
20
17
  from slack_sdk.socket_mode.response import SocketModeResponse
21
18
  from slack_sdk.web import WebClient
22
19
 
20
+ from .adapter_config import SlackAdapterConfig
21
+ from .message import SlackMessage
22
+
23
23
  T = TypeVar("T")
24
24
  log = getLogger(__file__)
25
25
 
26
26
 
27
- __all__ = ("SlackMessage", "mention_user", "SlackAdapterManager", "SlackInputAdapterImpl", "SlackOutputAdapterImpl")
28
-
29
-
30
- class SlackMessage(Struct):
31
- user: str
32
- user_email: str # email of the author
33
- user_id: str # user id of the author
34
- tags: List[str] # list of mentions
35
-
36
- channel: str # name of channel
37
- channel_id: str # id of channel
38
- channel_type: str # type of channel, in "message", "public" (app_mention), "private" (app_mention)
39
-
40
- msg: str # parsed text payload
41
- reaction: str # emoji reacts
42
- thread: str # thread id, if in thread
43
- payload: dict # raw message payload
44
-
45
-
46
- def mention_user(userid: str) -> str:
47
- """Convenience method, more difficult to do in symphony but we want slack to be symmetric"""
48
- return f"<@{userid}>"
27
+ __all__ = ("SlackAdapterManager", "SlackInputAdapterImpl", "SlackOutputAdapterImpl")
49
28
 
50
29
 
51
30
  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
-
31
+ def __init__(self, config: SlackAdapterConfig):
56
32
  self._slack_client = SocketModeClient(
57
- app_token=app_token,
58
- web_client=WebClient(token=bot_token, ssl=ssl),
33
+ app_token=config.app_token,
34
+ web_client=WebClient(token=config.bot_token, ssl=config.ssl),
59
35
  )
60
36
  self._slack_client.socket_mode_request_listeners.append(self._process_slack_message)
61
37
 
@@ -72,14 +48,18 @@ class SlackAdapterManager(AdapterManagerImpl):
72
48
  self._thread: Thread = None
73
49
 
74
50
  # lookups for mentions and redirection
75
- self._room_id_to_room_name: Dict[str, str] = {}
76
- self._room_id_to_room_type: Dict[str, str] = {}
77
- self._room_name_to_room_id: Dict[str, str] = {}
51
+ self._channel_id_to_channel_name: Dict[str, str] = {}
52
+ self._channel_id_to_channel_type: Dict[str, str] = {}
53
+ self._channel_name_to_channel_id: Dict[str, str] = {}
78
54
  self._user_id_to_user_name: Dict[str, str] = {}
79
55
  self._user_id_to_user_email: Dict[str, str] = {}
80
56
  self._user_name_to_user_id: Dict[str, str] = {}
81
57
  self._user_email_to_user_id: Dict[str, str] = {}
82
58
 
59
+ # if subscribed to mentions AND events, will get 2 copies,
60
+ # so we want to dedupe by id
61
+ self._seen_msg_ids = set()
62
+
83
63
  def subscribe(self):
84
64
  return _slack_input_adapter(self, push_mode=csp.PushMode.NON_COLLAPSING)
85
65
 
@@ -121,7 +101,7 @@ class SlackAdapterManager(AdapterManagerImpl):
121
101
  if ret.status_code == 200:
122
102
  # TODO OAuth scopes required
123
103
  name = ret.data["user"]["profile"].get("real_name_normalized", ret.data["user"]["name"])
124
- email = ret.data["user"]["profile"]["email"]
104
+ email = ret.data["user"]["profile"].get("email", "")
125
105
  self._user_id_to_user_name[user_id] = name
126
106
  self._user_name_to_user_id[name] = user_id # TODO is this 1-1 in slack?
127
107
  self._user_id_to_user_email[user_id] = email
@@ -140,14 +120,33 @@ class SlackAdapterManager(AdapterManagerImpl):
140
120
  if ret.status_code == 200:
141
121
  # TODO OAuth scopes required
142
122
  for user in ret.data["members"]:
123
+ # Grab name
143
124
  name = user["profile"].get("real_name_normalized", user["name"])
144
- user_id = user["profile"]["id"]
145
- email = user["profile"]["email"]
125
+
126
+ # Try to grab id
127
+ if "id" in user:
128
+ user_id = user["id"]
129
+ elif "id" in user["profile"]:
130
+ user_id = user["profile"]["id"]
131
+ else:
132
+ raise RuntimeError(f"No id found in user profile: {user}")
133
+
134
+ # Try to grab email
135
+ if "email" in user["profile"]:
136
+ email = user["profile"]["email"]
137
+ else:
138
+ log.warning(f"No email found in user profile, using id: {user}")
139
+ email = user_id
140
+
146
141
  self._user_id_to_user_name[user_id] = name
147
142
  self._user_name_to_user_id[name] = user_id # TODO is this 1-1 in slack?
148
143
  self._user_id_to_user_email[user_id] = email
149
144
  self._user_email_to_user_id[email] = user_id
150
- return self._user_name_to_user_id.get(user_name, None)
145
+
146
+ user_id = self._user_name_to_user_id.get(user_name, None)
147
+ if user_id is None:
148
+ # no user found
149
+ raise ValueError(f"User {user_name} not found in Slack")
151
150
  return user_id
152
151
 
153
152
  def _channel_data_to_channel_kind(self, data) -> str:
@@ -159,8 +158,8 @@ class SlackAdapterManager(AdapterManagerImpl):
159
158
 
160
159
  def _get_channel_from_id(self, channel_id):
161
160
  # try to pull from cache
162
- name = self._room_id_to_room_name.get(channel_id, None)
163
- kind = self._room_id_to_room_type.get(channel_id, None)
161
+ name = self._channel_id_to_channel_name.get(channel_id, None)
162
+ kind = self._channel_id_to_channel_type.get(channel_id, None)
164
163
 
165
164
  # if none, refresh data via web client
166
165
  if name is None:
@@ -170,18 +169,29 @@ class SlackAdapterManager(AdapterManagerImpl):
170
169
  kind = self._channel_data_to_channel_kind(ret.data["channel"])
171
170
  if kind == "message":
172
171
  # TODO use same behavior as symphony adapter
173
- name = "DM"
172
+ name = "IM"
174
173
  else:
175
174
  name = ret.data["channel"]["name"]
176
175
 
177
- self._room_id_to_room_name[channel_id] = name
178
- self._room_name_to_room_id[name] = channel_id
179
- self._room_id_to_room_type[channel_id] = kind
176
+ if name == "IM":
177
+ # store by the name of the user
178
+ user = ret.data["channel"]["user"]
179
+ user_name = self._get_user_from_id(user)[0]
180
+ self._channel_name_to_channel_id[user_name] = channel_id
181
+ else:
182
+ self._channel_name_to_channel_id[name] = channel_id
183
+ self._channel_id_to_channel_name[channel_id] = name
184
+ self._channel_id_to_channel_type[channel_id] = kind
180
185
  return name, kind
181
186
 
182
187
  def _get_channel_from_name(self, channel_name):
183
- # try to pull from cache
184
- channel_id = self._room_name_to_room_id.get(channel_name, None)
188
+ # first, see if its a regular name or tagged name
189
+ if channel_name.startswith("<#") and channel_name.endswith("|>"):
190
+ # strip out the tag
191
+ channel_id = channel_name[2:-2]
192
+ else:
193
+ # try to pull from cache
194
+ channel_id = self._channel_name_to_channel_id.get(channel_name, None)
185
195
 
186
196
  # if none, refresh data via web client
187
197
  if channel_id is None:
@@ -194,20 +204,17 @@ class SlackAdapterManager(AdapterManagerImpl):
194
204
  name = channel["name"]
195
205
  channel_id = channel["id"]
196
206
  kind = self._channel_data_to_channel_kind(channel)
197
- self._room_id_to_room_name[channel_id] = name
198
- self._room_name_to_room_id[name] = channel_id
199
- self._room_id_to_room_type[channel_id] = kind
200
- return self._room_name_to_room_id.get(channel_name, None)
207
+ self._channel_id_to_channel_name[channel_id] = name
208
+ self._channel_name_to_channel_id[name] = channel_id
209
+ self._channel_id_to_channel_type[channel_id] = kind
210
+ channel_id = self._channel_name_to_channel_id.get(channel_name, None)
211
+ if channel_id is None:
212
+ # no channel found
213
+ raise ValueError(f"Channel {channel_name} not found in Slack")
201
214
  return channel_id
202
215
 
203
- def _get_tags_from_message(self, blocks, authorizations=None) -> List[str]:
216
+ def _get_tags_from_message(self, blocks) -> List[str]:
204
217
  """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
218
  tags = []
212
219
  to_search = blocks.copy()
213
220
 
@@ -219,11 +226,9 @@ class SlackAdapterManager(AdapterManagerImpl):
219
226
 
220
227
  if element.get("type", "") == "user":
221
228
  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)
229
+ name, _ = self._get_user_from_id(tag_id)
230
+ if name:
231
+ tags.append(name)
227
232
  return tags
228
233
 
229
234
  def _process_slack_message(self, client: SocketModeClient, req: SocketModeRequest):
@@ -233,10 +238,19 @@ class SlackAdapterManager(AdapterManagerImpl):
233
238
  response = SocketModeResponse(envelope_id=req.envelope_id)
234
239
  client.send_socket_mode_response(response)
235
240
 
241
+ if req.payload["event"]["ts"] in self._seen_msg_ids:
242
+ # already seen, pop it and move on
243
+ self._seen_msg_ids.remove(req.payload["event"]["ts"])
244
+ return
245
+
246
+ # else add it so we don't process it again
247
+ self._seen_msg_ids.add(req.payload["event"]["ts"])
248
+
236
249
  if req.payload["event"]["type"] in ("message", "app_mention") and req.payload["event"].get("subtype") is None:
237
250
  user, user_email = self._get_user_from_id(req.payload["event"]["user"])
238
251
  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"])
252
+
253
+ tags = self._get_tags_from_message(req.payload["event"]["blocks"])
240
254
  slack_msg = SlackMessage(
241
255
  user=user or "",
242
256
  user_email=user_email or "",
@@ -265,8 +279,8 @@ class SlackAdapterManager(AdapterManagerImpl):
265
279
  # grab channel or DM
266
280
  if hasattr(slack_msg, "channel_id") and slack_msg.channel_id:
267
281
  channel_id = slack_msg.channel_id
282
+
268
283
  elif hasattr(slack_msg, "channel") and slack_msg.channel:
269
- # TODO DM
270
284
  channel_id = self._get_channel_from_name(slack_msg.channel)
271
285
 
272
286
  # pull text or reaction
@@ -277,6 +291,7 @@ class SlackAdapterManager(AdapterManagerImpl):
277
291
  name=slack_msg.reaction,
278
292
  timestamp=slack_msg.thread,
279
293
  )
294
+
280
295
  elif hasattr(slack_msg, "msg") and slack_msg.msg:
281
296
  try:
282
297
  # send text to channel
@@ -285,11 +300,10 @@ class SlackAdapterManager(AdapterManagerImpl):
285
300
  text=getattr(slack_msg, "msg", ""),
286
301
  )
287
302
  except SlackApiError:
288
- # TODO
289
- ...
303
+ log.exception("Failed to send message to Slack")
290
304
  else:
291
305
  # cannot send empty message, log an error
292
- log.error(f"Received malformed SlackMessage instance: {slack_msg}")
306
+ log.exception(f"Received malformed SlackMessage instance: {slack_msg}")
293
307
 
294
308
  if not self._inqueue.empty():
295
309
  # pull all SlackMessages from queue
@@ -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
@@ -0,0 +1,44 @@
1
+ import csp
2
+ from csp import ts
3
+
4
+ from csp_adapter_slack import SlackAdapterConfig, SlackAdapterManager, SlackMessage
5
+
6
+ config = SlackAdapterConfig(
7
+ app_token=".app_token",
8
+ bot_token=".bot_token",
9
+ )
10
+
11
+
12
+ @csp.node
13
+ def add_reaction_when_mentioned(msg: ts[SlackMessage]) -> ts[SlackMessage]:
14
+ """Add a reaction to every message that starts with hello."""
15
+ if msg.msg.lower().startswith("hello"):
16
+ return SlackMessage(
17
+ channel=msg.channel,
18
+ thread=msg.thread,
19
+ reaction="wave",
20
+ )
21
+
22
+
23
+ def graph():
24
+ # Create a DiscordAdapter object
25
+ adapter = SlackAdapterManager(config)
26
+
27
+ # Subscribe and unroll the messages
28
+ msgs = csp.unroll(adapter.subscribe())
29
+
30
+ # Print it out locally for debugging
31
+ csp.print("msgs", msgs)
32
+
33
+ # Add the reaction node
34
+ reactions = add_reaction_when_mentioned(msgs)
35
+
36
+ # Print it out locally for debugging
37
+ csp.print("reactions", reactions)
38
+
39
+ # Publish the reactions
40
+ adapter.publish(reactions)
41
+
42
+
43
+ if __name__ == "__main__":
44
+ csp.run(graph, realtime=True)
@@ -0,0 +1,10 @@
1
+ __all__ = ("mention_user",)
2
+
3
+
4
+ def mention_user(userid: str) -> str:
5
+ """Convenience method, more difficult to do in symphony but we want slack to be symmetric"""
6
+ if userid.startswith("<@") and userid.endswith(">"):
7
+ return userid
8
+ if userid.startswith("@"):
9
+ return f"<{userid}>"
10
+ return f"<@{userid}>"
@@ -0,0 +1,40 @@
1
+ from typing import List
2
+
3
+ from csp.impl.struct import Struct
4
+
5
+ __all__ = ("SlackMessage",)
6
+
7
+
8
+ class SlackMessage(Struct):
9
+ user: str
10
+ """name of the author of the message"""
11
+
12
+ user_email: str
13
+ """email of the author of the message, if available"""
14
+
15
+ user_id: str
16
+ """platform-specific id of the author of the message, if available"""
17
+
18
+ tags: List[str]
19
+ """list of users tagged in the `msg` of the message"""
20
+
21
+ channel: str
22
+ """name of the channel for the slack message, if available"""
23
+
24
+ channel_id: str
25
+ """id of the channel for the slack message, if available"""
26
+
27
+ channel_type: str
28
+ """type of the channel. either "message", "public", or "private" """
29
+
30
+ msg: str
31
+ """parsed text of the message"""
32
+
33
+ reaction: str
34
+ """emoji reaction to put on a thread. Exclusive with `msg`, requires `thread`"""
35
+
36
+ thread: str
37
+ """thread to post msg under, or msg id on which to apply reaction"""
38
+
39
+ payload: dict
40
+ """raw slack message payload"""
@@ -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):
@@ -130,15 +134,15 @@ class TestSlack:
130
134
  reqmock.type = "events_api"
131
135
  reqmock.payload = payload
132
136
 
133
- # mock out the user/room lookup responses
137
+ # mock out the user/channel lookup responses
134
138
  mock_user_response = MagicMock(name="users_info_mock")
135
139
  mock_user_response.status_code = 200
136
140
  mock_user_response.data = {"user": {"profile": {"real_name_normalized": "johndoe", "email": "johndoe@some.email"}, "name": "blerg"}}
137
141
  clientmock.return_value.web_client.users_info.return_value = mock_user_response
138
- mock_room_response = MagicMock(name="conversations_info_mock")
139
- mock_room_response.status_code = 200
140
- mock_room_response.data = {"channel": {"is_im": False, "is_private": True, "name": "a private channel"}}
141
- clientmock.return_value.web_client.conversations_info.return_value = mock_room_response
142
+ mock_channel_response = MagicMock(name="conversations_info_mock")
143
+ mock_channel_response.status_code = 200
144
+ mock_channel_response.data = {"channel": {"is_im": False, "is_private": True, "name": "a private channel"}}
145
+ clientmock.return_value.web_client.conversations_info.return_value = mock_channel_response
142
146
  mock_list_response = MagicMock(name="conversations_list_mock")
143
147
  mock_list_response.status_code = 200
144
148
  mock_list_response.data = {
@@ -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)
@@ -189,7 +193,7 @@ class TestSlack:
189
193
 
190
194
  # check all inbound mocks got called
191
195
  if payload == PUBLIC_CHANNEL_MENTION_PAYLOAD:
192
- assert clientmock.return_value.web_client.users_info.call_count == 2
196
+ assert clientmock.return_value.web_client.users_info.call_count == 3
193
197
  else:
194
198
  assert clientmock.return_value.web_client.users_info.call_count == 1
195
199
  assert clientmock.return_value.web_client.conversations_info.call_count == 1
@@ -1,17 +1,17 @@
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.28",
4
+ "pkginfo>=1.10,<1.13",
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.3.0"
12
12
  readme = "README.md"
13
13
  license = { file = "LICENSE" }
14
- requires-python = ">=3.8"
14
+ requires-python = ">=3.9"
15
15
  authors = [{name = "the csp authors", email = "CSPOpenSource@point72.com"}]
16
16
  keywords = [
17
17
  "csp",
@@ -25,27 +25,29 @@ classifiers = [
25
25
  "Framework :: Jupyter",
26
26
  "Programming Language :: Python",
27
27
  "Programming Language :: Python :: 3",
28
- "Programming Language :: Python :: 3.8",
29
28
  "Programming Language :: Python :: 3.9",
30
29
  "Programming Language :: Python :: 3.10",
31
30
  "Programming Language :: Python :: 3.11",
32
31
  "Programming Language :: Python :: 3.12",
32
+ "Programming Language :: Python :: 3.13",
33
33
  "License :: OSI Approved :: Apache Software License",
34
34
  ]
35
35
  dependencies = [
36
36
  "csp",
37
+ "pydantic>=2",
37
38
  "slack-sdk>=3",
38
39
  ]
39
40
 
40
41
  [project.optional-dependencies]
41
42
  develop = [
42
- "bump2version>=1.0.0",
43
+ "bump-my-version",
43
44
  "check-manifest",
44
- "codespell>=2.2.6,<2.3",
45
+ "codespell>=2.2.6,<2.5",
45
46
  "hatchling",
46
47
  "mdformat>=0.7.17,<0.8",
47
- "ruff>=0.5,<0.6",
48
- "twine>=5,<5.2",
48
+ "mdformat-tables>=1,<1.1",
49
+ "ruff>=0.5,<0.12",
50
+ "twine>=5,<7",
49
51
  # test
50
52
  "pytest",
51
53
  "pytest-cov",
@@ -59,9 +61,38 @@ test = [
59
61
  Repository = "https://github.com/point72/csp-adapter-slack"
60
62
  Homepage = "https://github.com/point72/csp-adapter-slack"
61
63
 
64
+ [tool.bumpversion]
65
+ current_version = "0.3.0"
66
+ commit = true
67
+ tag = false
68
+ commit_args = "-s"
69
+
70
+ [[tool.bumpversion.files]]
71
+ filename = "csp_adapter_slack/__init__.py"
72
+ search = '__version__ = "{current_version}"'
73
+ replace = '__version__ = "{new_version}"'
74
+
75
+ [[tool.bumpversion.files]]
76
+ filename = "pyproject.toml"
77
+ search = 'version = "{current_version}"'
78
+ replace = 'version = "{new_version}"'
79
+
62
80
  [tool.check-manifest]
63
81
  ignore = []
64
82
 
83
+ [tool.coverage.run]
84
+ branch = true
85
+ omit = []
86
+
87
+ [tool.coverage.report]
88
+ exclude_also = [
89
+ "raise NotImplementedError",
90
+ "if __name__ == .__main__.:",
91
+ "@(abc\\.)?abstractmethod",
92
+ ]
93
+ ignore_errors = true
94
+ fail_under = 75
95
+
65
96
  [tool.hatch.build]
66
97
  artifacts = []
67
98
 
@@ -69,11 +100,7 @@ artifacts = []
69
100
  src = "/"
70
101
 
71
102
  [tool.hatch.build.targets.sdist]
72
- include = [
73
- "/csp_adapter_slack",
74
- "LICENSE",
75
- "README.md",
76
- ]
103
+ packages = ["csp_adapter_slack"]
77
104
  exclude = [
78
105
  "/.github",
79
106
  "/.gitignore",
@@ -81,9 +108,7 @@ exclude = [
81
108
  ]
82
109
 
83
110
  [tool.hatch.build.targets.wheel]
84
- include = [
85
- "/csp_adapter_slack",
86
- ]
111
+ packages = ["csp_adapter_slack"]
87
112
  exclude = [
88
113
  "/.github",
89
114
  "/.gitignore",
@@ -92,12 +117,16 @@ exclude = [
92
117
  ]
93
118
 
94
119
  [tool.pytest.ini_options]
120
+ addopts = ["-vvv", "--junitxml=junit.xml"]
95
121
  asyncio_mode = "strict"
96
122
  testpaths = "csp_adapter_slack/tests"
97
123
 
98
124
  [tool.ruff]
99
125
  line-length = 150
100
126
 
127
+ [tool.ruff.lint]
128
+ extend-select = ["I"]
129
+
101
130
  [tool.ruff.lint.per-file-ignores]
102
131
  "__init__.py" = ["F401", "F403"]
103
132
 
@@ -107,7 +136,8 @@ default-section = "third-party"
107
136
  known-first-party = ["csp_adapter_slack"]
108
137
  section-order = [
109
138
  "future",
139
+ "standard-library",
110
140
  "third-party",
111
141
  "first-party",
112
142
  "local-folder",
113
- ]
143
+ ]
@@ -1,3 +0,0 @@
1
- __version__ = "0.1.0"
2
-
3
- from .adapter import *