csp-adapter-slack 0.0.0__tar.gz → 0.1.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.
@@ -0,0 +1,152 @@
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+
6
+ # C extensions
7
+ *.so
8
+
9
+ # Distribution / packaging
10
+ .Python
11
+ env/
12
+ build/
13
+ develop-eggs/
14
+ dist/
15
+ downloads/
16
+ eggs/
17
+ .eggs/
18
+ lib/
19
+ lib64/
20
+ parts/
21
+ sdist/
22
+ var/
23
+ *.egg-info/
24
+ .installed.cfg
25
+ *.egg
26
+
27
+ # PyInstaller
28
+ # Usually these files are written by a python script from a template
29
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
30
+ *.manifest
31
+ *.spec
32
+
33
+ # Installer logs
34
+ pip-log.txt
35
+ pip-delete-this-directory.txt
36
+
37
+ # Unit test / coverage reports
38
+ htmlcov/
39
+ .tox/
40
+ .coverage
41
+ .coverage.*
42
+ .cache
43
+ python_junit.xml
44
+ junit.xml
45
+ nosetests.xml
46
+ coverage.xml
47
+ *,cover
48
+ .hypothesis/
49
+ .pytest_cache
50
+ .ruff_cache
51
+ js/playwright-report
52
+ js/test-results
53
+
54
+ # Translations
55
+ *.mo
56
+ *.pot
57
+
58
+ # Django stuff:
59
+ *.log
60
+ local_settings.py
61
+
62
+ # Flask instance folder
63
+ instance/
64
+
65
+ # Scrapy stuff:
66
+ .scrapy
67
+
68
+ # Sphinx documentation
69
+ docs/_build/
70
+ docs/source
71
+
72
+ # PyBuilder
73
+ target/
74
+
75
+ # IPython Notebook
76
+ .ipynb_checkpoints
77
+ *.ipynb
78
+ .autoversion
79
+
80
+ # pyenv
81
+ .python-version
82
+
83
+ # celery beat schedule file
84
+ celerybeat-schedule
85
+
86
+ # dotenv
87
+ .env
88
+
89
+ # virtualenv
90
+ venv/
91
+ ENV/
92
+
93
+ # Spyder project settings
94
+ .spyderproject
95
+
96
+ # Rope project settings
97
+ .ropeproject
98
+
99
+ # =========================
100
+ # Operating System Files
101
+ # =========================
102
+
103
+ # OSX
104
+ # =========================
105
+
106
+ .DS_Store
107
+ .AppleDouble
108
+ .LSOverride
109
+
110
+ # Thumbnails
111
+ ._*
112
+
113
+ # Files that might appear in the root of a volume
114
+ .DocumentRevisions-V100
115
+ .fseventsd
116
+ .Spotlight-V100
117
+ .TemporaryItems
118
+ .Trashes
119
+ .VolumeIcon.icns
120
+
121
+ # Directories potentially created on remote AFP share
122
+ .AppleDB
123
+ .AppleDesktop
124
+ Network Trash Folder
125
+ Temporary Items
126
+ .apdisk
127
+
128
+ # Windows
129
+ # =========================
130
+
131
+ # Windows image file caches
132
+ Thumbs.db
133
+ ehthumbs.db
134
+
135
+ # Folder config file
136
+ Desktop.ini
137
+
138
+ # Recycle Bin used on file shares
139
+ $RECYCLE.BIN/
140
+
141
+ # Windows Installer files
142
+ *.cab
143
+ *.msi
144
+ *.msm
145
+ *.msp
146
+
147
+ # Windows shortcuts
148
+ *.lnk
149
+
150
+ # Coverage data
151
+ # -------------
152
+ **/coverage/
@@ -186,7 +186,7 @@
186
186
  same "printed page" as the copyright notice for easier
187
187
  identification within third-party archives.
188
188
 
189
- Copyright 2023 Point72, L.P.
189
+ Copyright 2024 Point72, L..P.
190
190
 
191
191
  Licensed under the Apache License, Version 2.0 (the "License");
192
192
  you may not use this file except in compliance with the License.
@@ -1,7 +1,9 @@
1
- Metadata-Version: 2.1
2
- Name: csp-adapter-slack
3
- Version: 0.0.0
4
- Summary: CSP adapter
1
+ Metadata-Version: 2.3
2
+ Name: csp_adapter_slack
3
+ Version: 0.1.0
4
+ Summary: A csp adapter for slack
5
+ Project-URL: Repository, https://github.com/point72/csp-adapter-slack
6
+ Project-URL: Homepage, https://github.com/point72/csp-adapter-slack
5
7
  Author-email: the csp authors <CSPOpenSource@point72.com>
6
8
  License: Apache License
7
9
  Version 2.0, January 2004
@@ -191,7 +193,7 @@ License: Apache License
191
193
  same "printed page" as the copyright notice for easier
192
194
  identification within third-party archives.
193
195
 
194
- Copyright 2023 Point72, L.P.
196
+ Copyright 2024 Point72, L..P.
195
197
 
196
198
  Licensed under the Apache License, Version 2.0 (the "License");
197
199
  you may not use this file except in compliance with the License.
@@ -204,21 +206,63 @@ License: Apache License
204
206
  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
205
207
  See the License for the specific language governing permissions and
206
208
  limitations under the License.
207
-
208
- Project-URL: Repository, https://github.com/point72/csp
209
- Project-URL: Homepage, https://github.com/Point72/csp
210
- Project-URL: Documentation, https://github.com/Point72/csp/wiki
211
- Project-URL: Tracker, https://github.com/point72/csp/issues
209
+ License-File: LICENSE
210
+ Keywords: chat,chatbot,csp,slack,stream-processing
212
211
  Classifier: Development Status :: 4 - Beta
212
+ Classifier: Framework :: Jupyter
213
+ Classifier: License :: OSI Approved :: Apache Software License
213
214
  Classifier: Programming Language :: Python
214
215
  Classifier: Programming Language :: Python :: 3
215
216
  Classifier: Programming Language :: Python :: 3.8
216
217
  Classifier: Programming Language :: Python :: 3.9
217
218
  Classifier: Programming Language :: Python :: 3.10
218
219
  Classifier: Programming Language :: Python :: 3.11
220
+ Classifier: Programming Language :: Python :: 3.12
219
221
  Requires-Python: >=3.8
220
- Description-Content-Type: text/markdown
221
- License-File: LICENSE
222
222
  Requires-Dist: csp
223
+ Requires-Dist: slack-sdk>=3
224
+ Provides-Extra: develop
225
+ Requires-Dist: bump2version>=1.0.0; extra == 'develop'
226
+ Requires-Dist: check-manifest; extra == 'develop'
227
+ Requires-Dist: codespell<2.3,>=2.2.6; extra == 'develop'
228
+ Requires-Dist: hatchling; extra == 'develop'
229
+ Requires-Dist: mdformat<0.8,>=0.7.17; extra == 'develop'
230
+ Requires-Dist: pytest; extra == 'develop'
231
+ 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
+ Provides-Extra: test
235
+ Requires-Dist: pytest; extra == 'test'
236
+ Requires-Dist: pytest-cov; extra == 'test'
237
+ Description-Content-Type: text/markdown
238
+
239
+ # csp slack adapter
240
+
241
+ A [csp](https://github.com/point72/csp) adapter for [slack](https://slack.com)
242
+
243
+ [![Build Status](https://github.com/point72/csp-adapter-slack/actions/workflows/build.yml/badge.svg?branch=main)](https://github.com/point72/csp-adapter-slack/actions?query=workflow%3A%22Build+Status%22)
244
+ [![GitHub issues](https://img.shields.io/github/issues/point72/csp-adapter-slack.svg)](https://github.com/point72/csp-adapter-slack/issues)
245
+ [![PyPI](https://img.shields.io/pypi/l/csp-adapter-slack.svg)](https://pypi.python.org/pypi/csp-adapter-slack)
246
+ [![PyPI](https://img.shields.io/pypi/v/csp-adapter-slack.svg)](https://pypi.python.org/pypi/csp-adapter-slack)
247
+
248
+ ## Features
249
+
250
+ [More information is available in our wiki](https://github.com/Point72/csp-adapter-slack/wiki)
251
+
252
+ ## Installation
253
+
254
+ Install with `pip`:
255
+
256
+ ```bash
257
+ pip install csp csp-adapter-slack
258
+ ```
259
+
260
+ Install with `conda`
261
+
262
+ ```bash
263
+ conda install csp csp-adapter-slack -c conda-forge
264
+ ```
265
+
266
+ ## License
223
267
 
224
- # csp-adapter-slack
268
+ This software is licensed under the Apache 2.0 license. See the [LICENSE](LICENSE) file for details.
@@ -0,0 +1,30 @@
1
+ # csp slack adapter
2
+
3
+ A [csp](https://github.com/point72/csp) adapter for [slack](https://slack.com)
4
+
5
+ [![Build Status](https://github.com/point72/csp-adapter-slack/actions/workflows/build.yml/badge.svg?branch=main)](https://github.com/point72/csp-adapter-slack/actions?query=workflow%3A%22Build+Status%22)
6
+ [![GitHub issues](https://img.shields.io/github/issues/point72/csp-adapter-slack.svg)](https://github.com/point72/csp-adapter-slack/issues)
7
+ [![PyPI](https://img.shields.io/pypi/l/csp-adapter-slack.svg)](https://pypi.python.org/pypi/csp-adapter-slack)
8
+ [![PyPI](https://img.shields.io/pypi/v/csp-adapter-slack.svg)](https://pypi.python.org/pypi/csp-adapter-slack)
9
+
10
+ ## Features
11
+
12
+ [More information is available in our wiki](https://github.com/Point72/csp-adapter-slack/wiki)
13
+
14
+ ## Installation
15
+
16
+ Install with `pip`:
17
+
18
+ ```bash
19
+ pip install csp csp-adapter-slack
20
+ ```
21
+
22
+ Install with `conda`
23
+
24
+ ```bash
25
+ conda install csp csp-adapter-slack -c conda-forge
26
+ ```
27
+
28
+ ## License
29
+
30
+ This software is licensed under the Apache 2.0 license. See the [LICENSE](LICENSE) file for details.
@@ -0,0 +1,3 @@
1
+ __version__ = "0.1.0"
2
+
3
+ from .adapter import *
@@ -0,0 +1,357 @@
1
+ import threading
2
+ from logging import getLogger
3
+ from queue import Queue
4
+ from ssl import SSLContext
5
+ from threading import Thread
6
+ from time import sleep
7
+ from typing import Dict, List, Optional, TypeVar
8
+
9
+ import csp
10
+ from csp.impl.adaptermanager import AdapterManagerImpl
11
+ from csp.impl.outputadapter import OutputAdapter
12
+ from csp.impl.pushadapter import PushInputAdapter
13
+ from csp.impl.struct import Struct
14
+ from csp.impl.types.tstype import ts
15
+ from csp.impl.wiring import py_output_adapter_def, py_push_adapter_def
16
+
17
+ from slack_sdk.errors import SlackApiError
18
+ from slack_sdk.socket_mode import SocketModeClient
19
+ from slack_sdk.socket_mode.request import SocketModeRequest
20
+ from slack_sdk.socket_mode.response import SocketModeResponse
21
+ from slack_sdk.web import WebClient
22
+
23
+ T = TypeVar("T")
24
+ log = getLogger(__file__)
25
+
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}>"
49
+
50
+
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
+
56
+ self._slack_client = SocketModeClient(
57
+ app_token=app_token,
58
+ web_client=WebClient(token=bot_token, ssl=ssl),
59
+ )
60
+ self._slack_client.socket_mode_request_listeners.append(self._process_slack_message)
61
+
62
+ # down stream edges
63
+ self._subscribers = []
64
+ self._publishers = []
65
+
66
+ # message queues
67
+ self._inqueue: Queue[SlackMessage] = Queue()
68
+ self._outqueue: Queue[SlackMessage] = Queue()
69
+
70
+ # handler thread
71
+ self._running: bool = False
72
+ self._thread: Thread = None
73
+
74
+ # 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] = {}
78
+ self._user_id_to_user_name: Dict[str, str] = {}
79
+ self._user_id_to_user_email: Dict[str, str] = {}
80
+ self._user_name_to_user_id: Dict[str, str] = {}
81
+ self._user_email_to_user_id: Dict[str, str] = {}
82
+
83
+ def subscribe(self):
84
+ return _slack_input_adapter(self, push_mode=csp.PushMode.NON_COLLAPSING)
85
+
86
+ def publish(self, msg: ts[SlackMessage]):
87
+ return _slack_output_adapter(self, msg)
88
+
89
+ def _create(self, engine, memo):
90
+ # We'll avoid having a second class and make our AdapterManager and AdapterManagerImpl the same
91
+ super().__init__(engine)
92
+ return self
93
+
94
+ def start(self, starttime, endtime):
95
+ self._running = True
96
+ self._thread = threading.Thread(target=self._run, daemon=True)
97
+ self._thread.start()
98
+
99
+ def stop(self):
100
+ if self._running:
101
+ self._running = False
102
+ self._slack_client.close()
103
+ self._thread.join()
104
+
105
+ def register_subscriber(self, adapter):
106
+ if adapter not in self._subscribers:
107
+ self._subscribers.append(adapter)
108
+
109
+ def register_publisher(self, adapter):
110
+ if adapter not in self._publishers:
111
+ self._publishers.append(adapter)
112
+
113
+ def _get_user_from_id(self, user_id):
114
+ # try to pull from cache
115
+ name = self._user_id_to_user_name.get(user_id, None)
116
+ email = self._user_id_to_user_email.get(user_id, None)
117
+
118
+ # if none, refresh data via web client
119
+ if name is None or email is None:
120
+ ret = self._slack_client.web_client.users_info(user=user_id)
121
+ if ret.status_code == 200:
122
+ # TODO OAuth scopes required
123
+ name = ret.data["user"]["profile"].get("real_name_normalized", ret.data["user"]["name"])
124
+ email = ret.data["user"]["profile"]["email"]
125
+ self._user_id_to_user_name[user_id] = name
126
+ self._user_name_to_user_id[name] = user_id # TODO is this 1-1 in slack?
127
+ self._user_id_to_user_email[user_id] = email
128
+ self._user_email_to_user_id[email] = user_id
129
+ return name, email
130
+
131
+ def _get_user_from_name(self, user_name):
132
+ # try to pull from cache
133
+ user_id = self._user_name_to_user_id.get(user_name, None)
134
+
135
+ # if none, refresh data via web client
136
+ if user_id is None:
137
+ # unfortunately the reverse lookup is not super nice...
138
+ # we need to pull all users and build the reverse mapping
139
+ ret = self._slack_client.web_client.users_list()
140
+ if ret.status_code == 200:
141
+ # TODO OAuth scopes required
142
+ for user in ret.data["members"]:
143
+ name = user["profile"].get("real_name_normalized", user["name"])
144
+ user_id = user["profile"]["id"]
145
+ email = user["profile"]["email"]
146
+ self._user_id_to_user_name[user_id] = name
147
+ self._user_name_to_user_id[name] = user_id # TODO is this 1-1 in slack?
148
+ self._user_id_to_user_email[user_id] = email
149
+ self._user_email_to_user_id[email] = user_id
150
+ return self._user_name_to_user_id.get(user_name, None)
151
+ return user_id
152
+
153
+ def _channel_data_to_channel_kind(self, data) -> str:
154
+ if data.get("is_im", False):
155
+ return "message"
156
+ if data.get("is_private", False):
157
+ return "private"
158
+ return "public"
159
+
160
+ def _get_channel_from_id(self, channel_id):
161
+ # 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)
164
+
165
+ # if none, refresh data via web client
166
+ if name is None:
167
+ ret = self._slack_client.web_client.conversations_info(channel=channel_id)
168
+ if ret.status_code == 200:
169
+ # TODO OAuth scopes required
170
+ kind = self._channel_data_to_channel_kind(ret.data["channel"])
171
+ if kind == "message":
172
+ # TODO use same behavior as symphony adapter
173
+ name = "DM"
174
+ else:
175
+ name = ret.data["channel"]["name"]
176
+
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
180
+ return name, kind
181
+
182
+ 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)
185
+
186
+ # if none, refresh data via web client
187
+ if channel_id is None:
188
+ # unfortunately the reverse lookup is not super nice...
189
+ # we need to pull all channels and build the reverse mapping
190
+ ret = self._slack_client.web_client.conversations_list()
191
+ if ret.status_code == 200:
192
+ # TODO OAuth scopes required
193
+ for channel in ret.data["channels"]:
194
+ name = channel["name"]
195
+ channel_id = channel["id"]
196
+ 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)
201
+ return channel_id
202
+
203
+ def _get_tags_from_message(self, blocks, authorizations=None) -> List[str]:
204
+ """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
+ tags = []
212
+ to_search = blocks.copy()
213
+
214
+ while to_search:
215
+ element = to_search.pop()
216
+ # add subsections
217
+ if element.get("elements", []):
218
+ to_search.extend(element.get("elements"))
219
+
220
+ if element.get("type", "") == "user":
221
+ 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)
227
+ return tags
228
+
229
+ def _process_slack_message(self, client: SocketModeClient, req: SocketModeRequest):
230
+ log.info(req.payload)
231
+ if req.type == "events_api":
232
+ # Acknowledge the request anyway
233
+ response = SocketModeResponse(envelope_id=req.envelope_id)
234
+ client.send_socket_mode_response(response)
235
+
236
+ if req.payload["event"]["type"] in ("message", "app_mention") and req.payload["event"].get("subtype") is None:
237
+ user, user_email = self._get_user_from_id(req.payload["event"]["user"])
238
+ 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"])
240
+ slack_msg = SlackMessage(
241
+ user=user or "",
242
+ user_email=user_email or "",
243
+ user_id=req.payload["event"]["user"],
244
+ tags=tags,
245
+ channel=channel or "",
246
+ channel_id=req.payload["event"]["channel"],
247
+ channel_type=channel_type or "",
248
+ msg=req.payload["event"]["text"],
249
+ reaction="",
250
+ thread=req.payload["event"]["ts"],
251
+ payload=req.payload.copy(),
252
+ )
253
+ self._inqueue.put(slack_msg)
254
+
255
+ def _run(self):
256
+ self._slack_client.connect()
257
+
258
+ while self._running:
259
+ # drain outbound
260
+ while not self._outqueue.empty():
261
+ # pull SlackMessage from queue
262
+ slack_msg = self._outqueue.get()
263
+
264
+ # refactor into slack command
265
+ # grab channel or DM
266
+ if hasattr(slack_msg, "channel_id") and slack_msg.channel_id:
267
+ channel_id = slack_msg.channel_id
268
+ elif hasattr(slack_msg, "channel") and slack_msg.channel:
269
+ # TODO DM
270
+ channel_id = self._get_channel_from_name(slack_msg.channel)
271
+
272
+ # pull text or reaction
273
+ if hasattr(slack_msg, "reaction") and slack_msg.reaction and hasattr(slack_msg, "thread") and slack_msg.thread:
274
+ # TODO
275
+ self._slack_client.web_client.reactions_add(
276
+ channel=channel_id,
277
+ name=slack_msg.reaction,
278
+ timestamp=slack_msg.thread,
279
+ )
280
+ elif hasattr(slack_msg, "msg") and slack_msg.msg:
281
+ try:
282
+ # send text to channel
283
+ self._slack_client.web_client.chat_postMessage(
284
+ channel=channel_id,
285
+ text=getattr(slack_msg, "msg", ""),
286
+ )
287
+ except SlackApiError:
288
+ # TODO
289
+ ...
290
+ else:
291
+ # cannot send empty message, log an error
292
+ log.error(f"Received malformed SlackMessage instance: {slack_msg}")
293
+
294
+ if not self._inqueue.empty():
295
+ # pull all SlackMessages from queue
296
+ # do as burst to match SymphonyAdapter
297
+ slack_msgs = []
298
+ while not self._inqueue.empty():
299
+ slack_msgs.append(self._inqueue.get())
300
+
301
+ # push to all the subscribers
302
+ for adapter in self._subscribers:
303
+ adapter.push_tick(slack_msgs)
304
+
305
+ # do short sleep
306
+ sleep(0.1)
307
+
308
+ # liveness check
309
+ if not self._thread.is_alive():
310
+ self._running = False
311
+ self._thread.join()
312
+
313
+ # shut down socket client
314
+ try:
315
+ # TODO which one?
316
+ self._slack_client.close()
317
+ # self._slack_client.disconnect()
318
+ except AttributeError:
319
+ # TODO bug in slack sdk causes an exception to be thrown
320
+ # File "slack_sdk/socket_mode/builtin/connection.py", line 191, in disconnect
321
+ # self.sock.close()
322
+ # ^^^^^^^^^^^^^^^
323
+ # AttributeError: 'NoneType' object has no attribute 'close'
324
+ ...
325
+
326
+ def _on_tick(self, value):
327
+ self._outqueue.put(value)
328
+
329
+
330
+ class SlackInputAdapterImpl(PushInputAdapter):
331
+ def __init__(self, manager):
332
+ manager.register_subscriber(self)
333
+ super().__init__()
334
+
335
+
336
+ class SlackOutputAdapterImpl(OutputAdapter):
337
+ def __init__(self, manager):
338
+ manager.register_publisher(self)
339
+ self._manager = manager
340
+ super().__init__()
341
+
342
+ def on_tick(self, time, value):
343
+ self._manager._on_tick(value)
344
+
345
+
346
+ _slack_input_adapter = py_push_adapter_def(
347
+ name="SlackInputAdapter",
348
+ adapterimpl=SlackInputAdapterImpl,
349
+ out_type=ts[[SlackMessage]],
350
+ manager_type=SlackAdapterManager,
351
+ )
352
+ _slack_output_adapter = py_output_adapter_def(
353
+ name="SlackOutputAdapter",
354
+ adapterimpl=SlackOutputAdapterImpl,
355
+ manager_type=SlackAdapterManager,
356
+ input=ts[SlackMessage],
357
+ )
@@ -0,0 +1,209 @@
1
+ import pytest
2
+ from datetime import timedelta
3
+ from ssl import create_default_context
4
+ from unittest.mock import MagicMock, call, patch
5
+
6
+ import csp
7
+ from csp import ts
8
+ from csp_adapter_slack import SlackAdapterManager, SlackMessage, mention_user
9
+
10
+
11
+ @csp.node
12
+ def hello(msg: ts[SlackMessage]) -> ts[SlackMessage]:
13
+ if csp.ticked(msg):
14
+ text = f"Hello <@{msg.user_id}>!"
15
+ return SlackMessage(
16
+ channel="a new channel",
17
+ # reply in thread
18
+ thread=msg.thread,
19
+ msg=text,
20
+ )
21
+
22
+
23
+ @csp.node
24
+ def react(msg: ts[SlackMessage]) -> ts[SlackMessage]:
25
+ if csp.ticked(msg):
26
+ return SlackMessage(
27
+ channel=msg.channel,
28
+ channel_id=msg.channel_id,
29
+ thread=msg.thread,
30
+ reaction="eyes",
31
+ )
32
+
33
+
34
+ @csp.node
35
+ def send_fake_message(clientmock: MagicMock, requestmock: MagicMock, am: SlackAdapterManager) -> ts[bool]:
36
+ with csp.alarms():
37
+ a_send = csp.alarm(bool)
38
+ with csp.start():
39
+ csp.schedule_alarm(a_send, timedelta(seconds=1), True)
40
+ if csp.ticked(a_send):
41
+ if a_send:
42
+ am._process_slack_message(clientmock, requestmock)
43
+ csp.schedule_alarm(a_send, timedelta(seconds=1), False)
44
+ else:
45
+ return True
46
+
47
+
48
+ PUBLIC_CHANNEL_MENTION_PAYLOAD = {
49
+ "token": "ABCD",
50
+ "team_id": "EFGH",
51
+ "api_app_id": "HIJK",
52
+ "event": {
53
+ "client_msg_id": "1234-5678",
54
+ "type": "app_mention",
55
+ "text": "<@BOTID> <@USERID> <@USERID2>",
56
+ "user": "USERID",
57
+ "ts": "1.2",
58
+ "blocks": [
59
+ {
60
+ "type": "rich_text",
61
+ "block_id": "tx381",
62
+ "elements": [
63
+ {
64
+ "type": "rich_text_section",
65
+ "elements": [
66
+ {"type": "user", "user_id": "BOTID"},
67
+ {"type": "text", "text": " "},
68
+ {"type": "user", "user_id": "USERID"},
69
+ {"type": "text", "text": " "},
70
+ {"type": "user", "user_id": "USERID2"},
71
+ ],
72
+ }
73
+ ],
74
+ }
75
+ ],
76
+ "team": "ABCD",
77
+ "channel": "EFGH",
78
+ "event_ts": "1.2",
79
+ },
80
+ "type": "event_callback",
81
+ "event_id": "ABCD",
82
+ "event_time": 1707423091,
83
+ "authorizations": [{"enterprise_id": None, "team_id": "ABCD", "user_id": "BOTID", "is_bot": True, "is_enterprise_install": False}],
84
+ "is_ext_shared_channel": False,
85
+ "event_context": "SOMELONGCONTEXT",
86
+ }
87
+ DIRECT_MESSAGE_PAYLOAD = {
88
+ "token": "ABCD",
89
+ "team_id": "EFGH",
90
+ "context_team_id": "ABCD",
91
+ "context_enterprise_id": None,
92
+ "api_app_id": "HIJK",
93
+ "event": {
94
+ "client_msg_id": "1234-5678",
95
+ "type": "message",
96
+ "text": "test",
97
+ "user": "USERID",
98
+ "ts": "2.1",
99
+ "blocks": [
100
+ {
101
+ "type": "rich_text",
102
+ "block_id": "gB9fq",
103
+ "elements": [{"type": "rich_text_section", "elements": [{"type": "text", "text": "test"}]}],
104
+ }
105
+ ],
106
+ "team": "ABCD",
107
+ "channel": "EFGH",
108
+ "event_ts": "2.1",
109
+ "channel_type": "im",
110
+ },
111
+ "type": "event_callback",
112
+ "event_id": "ABCD",
113
+ "event_time": 1707423220,
114
+ "authorizations": [{"enterprise_id": None, "team_id": "ABCD", "user_id": "BOTID", "is_bot": True, "is_enterprise_install": False}],
115
+ "is_ext_shared_channel": False,
116
+ "event_context": "SOMELONGCONTEXT",
117
+ }
118
+
119
+
120
+ class TestSlack:
121
+ def test_slack_tokens(self):
122
+ with pytest.raises(RuntimeError):
123
+ SlackAdapterManager("abc", "def")
124
+
125
+ @pytest.mark.parametrize("payload", (PUBLIC_CHANNEL_MENTION_PAYLOAD, DIRECT_MESSAGE_PAYLOAD))
126
+ def test_slack(self, payload):
127
+ with patch("csp_adapter_slack.adapter.SocketModeClient") as clientmock:
128
+ # mock out the event from the slack sdk
129
+ reqmock = MagicMock()
130
+ reqmock.type = "events_api"
131
+ reqmock.payload = payload
132
+
133
+ # mock out the user/room lookup responses
134
+ mock_user_response = MagicMock(name="users_info_mock")
135
+ mock_user_response.status_code = 200
136
+ mock_user_response.data = {"user": {"profile": {"real_name_normalized": "johndoe", "email": "johndoe@some.email"}, "name": "blerg"}}
137
+ 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_list_response = MagicMock(name="conversations_list_mock")
143
+ mock_list_response.status_code = 200
144
+ mock_list_response.data = {
145
+ "channels": [
146
+ {"name": "a private channel", "id": "EFGH"},
147
+ {"name": "a new channel", "id": "new_channel"},
148
+ ]
149
+ }
150
+ clientmock.return_value.web_client.conversations_list.return_value = mock_list_response
151
+
152
+ def graph():
153
+ am = SlackAdapterManager("xapp-1-dummy", "xoxb-dummy", ssl=create_default_context())
154
+
155
+ # send a fake slack message to the app
156
+ stop = send_fake_message(clientmock, reqmock, am)
157
+
158
+ # send a response
159
+ resp = hello(csp.unroll(am.subscribe()))
160
+ am.publish(resp)
161
+
162
+ # do a react
163
+ rct = react(csp.unroll(am.subscribe()))
164
+ am.publish(rct)
165
+
166
+ csp.add_graph_output("response", resp)
167
+ csp.add_graph_output("react", rct)
168
+
169
+ # stop after first messages
170
+ done_flag = (csp.count(stop) + csp.count(resp) + csp.count(rct)) == 3
171
+ csp.stop_engine(done_flag)
172
+
173
+ # run the graph
174
+ resp = csp.run(graph, realtime=True)
175
+
176
+ # check outputs
177
+ if payload == PUBLIC_CHANNEL_MENTION_PAYLOAD:
178
+ assert resp["react"]
179
+ assert resp["response"]
180
+
181
+ assert resp["react"][0][1] == SlackMessage(channel="a private channel", channel_id="EFGH", reaction="eyes", thread="1.2")
182
+ assert resp["response"][0][1] == SlackMessage(channel="a new channel", msg="Hello <@USERID>!", thread="1.2")
183
+ else:
184
+ assert resp["react"]
185
+ assert resp["response"]
186
+
187
+ assert resp["react"][0][1] == SlackMessage(channel="a private channel", channel_id="EFGH", reaction="eyes", thread="2.1")
188
+ assert resp["response"][0][1] == SlackMessage(channel="a new channel", msg="Hello <@USERID>!", thread="2.1")
189
+
190
+ # check all inbound mocks got called
191
+ if payload == PUBLIC_CHANNEL_MENTION_PAYLOAD:
192
+ assert clientmock.return_value.web_client.users_info.call_count == 2
193
+ else:
194
+ assert clientmock.return_value.web_client.users_info.call_count == 1
195
+ assert clientmock.return_value.web_client.conversations_info.call_count == 1
196
+
197
+ # check all outbound mocks got called
198
+ assert clientmock.return_value.web_client.reactions_add.call_count == 1
199
+ assert clientmock.return_value.web_client.chat_postMessage.call_count == 1
200
+
201
+ if payload == PUBLIC_CHANNEL_MENTION_PAYLOAD:
202
+ assert clientmock.return_value.web_client.reactions_add.call_args_list == [call(channel="EFGH", name="eyes", timestamp="1.2")]
203
+ assert clientmock.return_value.web_client.chat_postMessage.call_args_list == [call(channel="new_channel", text="Hello <@USERID>!")]
204
+ else:
205
+ assert clientmock.return_value.web_client.reactions_add.call_args_list == [call(channel="EFGH", name="eyes", timestamp="2.1")]
206
+ assert clientmock.return_value.web_client.chat_postMessage.call_args_list == [call(channel="new_channel", text="Hello <@USERID>!")]
207
+
208
+ def test_mention_user(self):
209
+ assert mention_user("ABCD") == "<@ABCD>"
@@ -0,0 +1,113 @@
1
+ [build-system]
2
+ requires = [
3
+ "hatchling>=1.22.4,<1.23",
4
+ "pkginfo>=1.10,<1.11",
5
+ ]
6
+ build-backend = "hatchling.build"
7
+
8
+ [project]
9
+ name = "csp_adapter_slack"
10
+ description = "A csp adapter for slack"
11
+ version = "0.1.0"
12
+ readme = "README.md"
13
+ license = { file = "LICENSE" }
14
+ requires-python = ">=3.8"
15
+ authors = [{name = "the csp authors", email = "CSPOpenSource@point72.com"}]
16
+ keywords = [
17
+ "csp",
18
+ "stream-processing",
19
+ "slack",
20
+ "chat",
21
+ "chatbot",
22
+ ]
23
+ classifiers = [
24
+ "Development Status :: 4 - Beta",
25
+ "Framework :: Jupyter",
26
+ "Programming Language :: Python",
27
+ "Programming Language :: Python :: 3",
28
+ "Programming Language :: Python :: 3.8",
29
+ "Programming Language :: Python :: 3.9",
30
+ "Programming Language :: Python :: 3.10",
31
+ "Programming Language :: Python :: 3.11",
32
+ "Programming Language :: Python :: 3.12",
33
+ "License :: OSI Approved :: Apache Software License",
34
+ ]
35
+ dependencies = [
36
+ "csp",
37
+ "slack-sdk>=3",
38
+ ]
39
+
40
+ [project.optional-dependencies]
41
+ develop = [
42
+ "bump2version>=1.0.0",
43
+ "check-manifest",
44
+ "codespell>=2.2.6,<2.3",
45
+ "hatchling",
46
+ "mdformat>=0.7.17,<0.8",
47
+ "ruff>=0.5,<0.6",
48
+ "twine>=5,<5.2",
49
+ # test
50
+ "pytest",
51
+ "pytest-cov",
52
+ ]
53
+ test = [
54
+ "pytest",
55
+ "pytest-cov",
56
+ ]
57
+
58
+ [project.urls]
59
+ Repository = "https://github.com/point72/csp-adapter-slack"
60
+ Homepage = "https://github.com/point72/csp-adapter-slack"
61
+
62
+ [tool.check-manifest]
63
+ ignore = []
64
+
65
+ [tool.hatch.build]
66
+ artifacts = []
67
+
68
+ [tool.hatch.build.sources]
69
+ src = "/"
70
+
71
+ [tool.hatch.build.targets.sdist]
72
+ include = [
73
+ "/csp_adapter_slack",
74
+ "LICENSE",
75
+ "README.md",
76
+ ]
77
+ exclude = [
78
+ "/.github",
79
+ "/.gitignore",
80
+ "/docs",
81
+ ]
82
+
83
+ [tool.hatch.build.targets.wheel]
84
+ include = [
85
+ "/csp_adapter_slack",
86
+ ]
87
+ exclude = [
88
+ "/.github",
89
+ "/.gitignore",
90
+ "/pyproject.toml",
91
+ "/docs",
92
+ ]
93
+
94
+ [tool.pytest.ini_options]
95
+ asyncio_mode = "strict"
96
+ testpaths = "csp_adapter_slack/tests"
97
+
98
+ [tool.ruff]
99
+ line-length = 150
100
+
101
+ [tool.ruff.lint.per-file-ignores]
102
+ "__init__.py" = ["F401", "F403"]
103
+
104
+ [tool.ruff.lint.isort]
105
+ combine-as-imports = true
106
+ default-section = "third-party"
107
+ known-first-party = ["csp_adapter_slack"]
108
+ section-order = [
109
+ "future",
110
+ "third-party",
111
+ "first-party",
112
+ "local-folder",
113
+ ]
@@ -1 +0,0 @@
1
- # csp-adapter-slack
@@ -1,224 +0,0 @@
1
- Metadata-Version: 2.1
2
- Name: csp-adapter-slack
3
- Version: 0.0.0
4
- Summary: CSP adapter
5
- Author-email: the csp authors <CSPOpenSource@point72.com>
6
- License: Apache License
7
- Version 2.0, January 2004
8
- http://www.apache.org/licenses/
9
-
10
- TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
11
-
12
- 1. Definitions.
13
-
14
- "License" shall mean the terms and conditions for use, reproduction,
15
- and distribution as defined by Sections 1 through 9 of this document.
16
-
17
- "Licensor" shall mean the copyright owner or entity authorized by
18
- the copyright owner that is granting the License.
19
-
20
- "Legal Entity" shall mean the union of the acting entity and all
21
- other entities that control, are controlled by, or are under common
22
- control with that entity. For the purposes of this definition,
23
- "control" means (i) the power, direct or indirect, to cause the
24
- direction or management of such entity, whether by contract or
25
- otherwise, or (ii) ownership of fifty percent (50%) or more of the
26
- outstanding shares, or (iii) beneficial ownership of such entity.
27
-
28
- "You" (or "Your") shall mean an individual or Legal Entity
29
- exercising permissions granted by this License.
30
-
31
- "Source" form shall mean the preferred form for making modifications,
32
- including but not limited to software source code, documentation
33
- source, and configuration files.
34
-
35
- "Object" form shall mean any form resulting from mechanical
36
- transformation or translation of a Source form, including but
37
- not limited to compiled object code, generated documentation,
38
- and conversions to other media types.
39
-
40
- "Work" shall mean the work of authorship, whether in Source or
41
- Object form, made available under the License, as indicated by a
42
- copyright notice that is included in or attached to the work
43
- (an example is provided in the Appendix below).
44
-
45
- "Derivative Works" shall mean any work, whether in Source or Object
46
- form, that is based on (or derived from) the Work and for which the
47
- editorial revisions, annotations, elaborations, or other modifications
48
- represent, as a whole, an original work of authorship. For the purposes
49
- of this License, Derivative Works shall not include works that remain
50
- separable from, or merely link (or bind by name) to the interfaces of,
51
- the Work and Derivative Works thereof.
52
-
53
- "Contribution" shall mean any work of authorship, including
54
- the original version of the Work and any modifications or additions
55
- to that Work or Derivative Works thereof, that is intentionally
56
- submitted to Licensor for inclusion in the Work by the copyright owner
57
- or by an individual or Legal Entity authorized to submit on behalf of
58
- the copyright owner. For the purposes of this definition, "submitted"
59
- means any form of electronic, verbal, or written communication sent
60
- to the Licensor or its representatives, including but not limited to
61
- communication on electronic mailing lists, source code control systems,
62
- and issue tracking systems that are managed by, or on behalf of, the
63
- Licensor for the purpose of discussing and improving the Work, but
64
- excluding communication that is conspicuously marked or otherwise
65
- designated in writing by the copyright owner as "Not a Contribution."
66
-
67
- "Contributor" shall mean Licensor and any individual or Legal Entity
68
- on behalf of whom a Contribution has been received by Licensor and
69
- subsequently incorporated within the Work.
70
-
71
- 2. Grant of Copyright License. Subject to the terms and conditions of
72
- this License, each Contributor hereby grants to You a perpetual,
73
- worldwide, non-exclusive, no-charge, royalty-free, irrevocable
74
- copyright license to reproduce, prepare Derivative Works of,
75
- publicly display, publicly perform, sublicense, and distribute the
76
- Work and such Derivative Works in Source or Object form.
77
-
78
- 3. Grant of Patent License. Subject to the terms and conditions of
79
- this License, each Contributor hereby grants to You a perpetual,
80
- worldwide, non-exclusive, no-charge, royalty-free, irrevocable
81
- (except as stated in this section) patent license to make, have made,
82
- use, offer to sell, sell, import, and otherwise transfer the Work,
83
- where such license applies only to those patent claims licensable
84
- by such Contributor that are necessarily infringed by their
85
- Contribution(s) alone or by combination of their Contribution(s)
86
- with the Work to which such Contribution(s) was submitted. If You
87
- institute patent litigation against any entity (including a
88
- cross-claim or counterclaim in a lawsuit) alleging that the Work
89
- or a Contribution incorporated within the Work constitutes direct
90
- or contributory patent infringement, then any patent licenses
91
- granted to You under this License for that Work shall terminate
92
- as of the date such litigation is filed.
93
-
94
- 4. Redistribution. You may reproduce and distribute copies of the
95
- Work or Derivative Works thereof in any medium, with or without
96
- modifications, and in Source or Object form, provided that You
97
- meet the following conditions:
98
-
99
- (a) You must give any other recipients of the Work or
100
- Derivative Works a copy of this License; and
101
-
102
- (b) You must cause any modified files to carry prominent notices
103
- stating that You changed the files; and
104
-
105
- (c) You must retain, in the Source form of any Derivative Works
106
- that You distribute, all copyright, patent, trademark, and
107
- attribution notices from the Source form of the Work,
108
- excluding those notices that do not pertain to any part of
109
- the Derivative Works; and
110
-
111
- (d) If the Work includes a "NOTICE" text file as part of its
112
- distribution, then any Derivative Works that You distribute must
113
- include a readable copy of the attribution notices contained
114
- within such NOTICE file, excluding those notices that do not
115
- pertain to any part of the Derivative Works, in at least one
116
- of the following places: within a NOTICE text file distributed
117
- as part of the Derivative Works; within the Source form or
118
- documentation, if provided along with the Derivative Works; or,
119
- within a display generated by the Derivative Works, if and
120
- wherever such third-party notices normally appear. The contents
121
- of the NOTICE file are for informational purposes only and
122
- do not modify the License. You may add Your own attribution
123
- notices within Derivative Works that You distribute, alongside
124
- or as an addendum to the NOTICE text from the Work, provided
125
- that such additional attribution notices cannot be construed
126
- as modifying the License.
127
-
128
- You may add Your own copyright statement to Your modifications and
129
- may provide additional or different license terms and conditions
130
- for use, reproduction, or distribution of Your modifications, or
131
- for any such Derivative Works as a whole, provided Your use,
132
- reproduction, and distribution of the Work otherwise complies with
133
- the conditions stated in this License.
134
-
135
- 5. Submission of Contributions. Unless You explicitly state otherwise,
136
- any Contribution intentionally submitted for inclusion in the Work
137
- by You to the Licensor shall be under the terms and conditions of
138
- this License, without any additional terms or conditions.
139
- Notwithstanding the above, nothing herein shall supersede or modify
140
- the terms of any separate license agreement you may have executed
141
- with Licensor regarding such Contributions.
142
-
143
- 6. Trademarks. This License does not grant permission to use the trade
144
- names, trademarks, service marks, or product names of the Licensor,
145
- except as required for reasonable and customary use in describing the
146
- origin of the Work and reproducing the content of the NOTICE file.
147
-
148
- 7. Disclaimer of Warranty. Unless required by applicable law or
149
- agreed to in writing, Licensor provides the Work (and each
150
- Contributor provides its Contributions) on an "AS IS" BASIS,
151
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
152
- implied, including, without limitation, any warranties or conditions
153
- of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
154
- PARTICULAR PURPOSE. You are solely responsible for determining the
155
- appropriateness of using or redistributing the Work and assume any
156
- risks associated with Your exercise of permissions under this License.
157
-
158
- 8. Limitation of Liability. In no event and under no legal theory,
159
- whether in tort (including negligence), contract, or otherwise,
160
- unless required by applicable law (such as deliberate and grossly
161
- negligent acts) or agreed to in writing, shall any Contributor be
162
- liable to You for damages, including any direct, indirect, special,
163
- incidental, or consequential damages of any character arising as a
164
- result of this License or out of the use or inability to use the
165
- Work (including but not limited to damages for loss of goodwill,
166
- work stoppage, computer failure or malfunction, or any and all
167
- other commercial damages or losses), even if such Contributor
168
- has been advised of the possibility of such damages.
169
-
170
- 9. Accepting Warranty or Additional Liability. While redistributing
171
- the Work or Derivative Works thereof, You may choose to offer,
172
- and charge a fee for, acceptance of support, warranty, indemnity,
173
- or other liability obligations and/or rights consistent with this
174
- License. However, in accepting such obligations, You may act only
175
- on Your own behalf and on Your sole responsibility, not on behalf
176
- of any other Contributor, and only if You agree to indemnify,
177
- defend, and hold each Contributor harmless for any liability
178
- incurred by, or claims asserted against, such Contributor by reason
179
- of your accepting any such warranty or additional liability.
180
-
181
- END OF TERMS AND CONDITIONS
182
-
183
- APPENDIX: How to apply the Apache License to your work.
184
-
185
- To apply the Apache License to your work, attach the following
186
- boilerplate notice, with the fields enclosed by brackets "[]"
187
- replaced with your own identifying information. (Don't include
188
- the brackets!) The text should be enclosed in the appropriate
189
- comment syntax for the file format. We also recommend that a
190
- file or class name and description of purpose be included on the
191
- same "printed page" as the copyright notice for easier
192
- identification within third-party archives.
193
-
194
- Copyright 2023 Point72, L.P.
195
-
196
- Licensed under the Apache License, Version 2.0 (the "License");
197
- you may not use this file except in compliance with the License.
198
- You may obtain a copy of the License at
199
-
200
- http://www.apache.org/licenses/LICENSE-2.0
201
-
202
- Unless required by applicable law or agreed to in writing, software
203
- distributed under the License is distributed on an "AS IS" BASIS,
204
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
205
- See the License for the specific language governing permissions and
206
- limitations under the License.
207
-
208
- Project-URL: Repository, https://github.com/point72/csp
209
- Project-URL: Homepage, https://github.com/Point72/csp
210
- Project-URL: Documentation, https://github.com/Point72/csp/wiki
211
- Project-URL: Tracker, https://github.com/point72/csp/issues
212
- Classifier: Development Status :: 4 - Beta
213
- Classifier: Programming Language :: Python
214
- Classifier: Programming Language :: Python :: 3
215
- Classifier: Programming Language :: Python :: 3.8
216
- Classifier: Programming Language :: Python :: 3.9
217
- Classifier: Programming Language :: Python :: 3.10
218
- Classifier: Programming Language :: Python :: 3.11
219
- Requires-Python: >=3.8
220
- Description-Content-Type: text/markdown
221
- License-File: LICENSE
222
- Requires-Dist: csp
223
-
224
- # csp-adapter-slack
@@ -1,9 +0,0 @@
1
- LICENSE
2
- README.md
3
- pyproject.toml
4
- setup.py
5
- csp_adapter_slack.egg-info/PKG-INFO
6
- csp_adapter_slack.egg-info/SOURCES.txt
7
- csp_adapter_slack.egg-info/dependency_links.txt
8
- csp_adapter_slack.egg-info/requires.txt
9
- csp_adapter_slack.egg-info/top_level.txt
@@ -1,30 +0,0 @@
1
- [build-system]
2
- requires = ["setuptools", "wheel"]
3
- build-backend = "setuptools.build_meta"
4
-
5
- [project]
6
- name = "csp-adapter-slack"
7
- description = "CSP adapter"
8
- version = "0.0.0"
9
- readme = "README.md"
10
- license = { file = "LICENSE" }
11
- requires-python = ">=3.8"
12
- authors = [{name = "the csp authors", email = "CSPOpenSource@point72.com"}]
13
- classifiers = [
14
- "Development Status :: 4 - Beta",
15
- "Programming Language :: Python",
16
- "Programming Language :: Python :: 3",
17
- "Programming Language :: Python :: 3.8",
18
- "Programming Language :: Python :: 3.9",
19
- "Programming Language :: Python :: 3.10",
20
- "Programming Language :: Python :: 3.11",
21
- ]
22
- dependencies = [
23
- "csp",
24
- ]
25
-
26
- [project.urls]
27
- Repository = "https://github.com/point72/csp"
28
- Homepage = "https://github.com/Point72/csp"
29
- Documentation = "https://github.com/Point72/csp/wiki"
30
- Tracker = "https://github.com/point72/csp/issues"
@@ -1,4 +0,0 @@
1
- [egg_info]
2
- tag_build =
3
- tag_date = 0
4
-
@@ -1,2 +0,0 @@
1
- # setup.py shim for use with applications that require it.
2
- __import__("setuptools").setup()