csp-adapter-slack 0.0.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.
@@ -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.2.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,65 @@ 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
+ Keywords: chat,chatbot,csp,slack,stream-processing
212
210
  Classifier: Development Status :: 4 - Beta
211
+ Classifier: Framework :: Jupyter
212
+ Classifier: License :: OSI Approved :: Apache Software License
213
213
  Classifier: Programming Language :: Python
214
214
  Classifier: Programming Language :: Python :: 3
215
215
  Classifier: Programming Language :: Python :: 3.8
216
216
  Classifier: Programming Language :: Python :: 3.9
217
217
  Classifier: Programming Language :: Python :: 3.10
218
218
  Classifier: Programming Language :: Python :: 3.11
219
+ Classifier: Programming Language :: Python :: 3.12
220
+ Classifier: Programming Language :: Python :: 3.13
219
221
  Requires-Python: >=3.8
220
- Description-Content-Type: text/markdown
221
- License-File: LICENSE
222
222
  Requires-Dist: csp
223
+ Requires-Dist: pydantic>=2
224
+ Requires-Dist: slack-sdk>=3
225
+ Provides-Extra: develop
226
+ Requires-Dist: bump-my-version; extra == 'develop'
227
+ Requires-Dist: check-manifest; extra == 'develop'
228
+ Requires-Dist: codespell<2.4,>=2.2.6; extra == 'develop'
229
+ Requires-Dist: hatchling; extra == 'develop'
230
+ Requires-Dist: mdformat-tables<1.1,>=1; extra == 'develop'
231
+ Requires-Dist: mdformat<0.8,>=0.7.17; extra == 'develop'
232
+ Requires-Dist: pytest; extra == 'develop'
233
+ Requires-Dist: pytest-cov; extra == 'develop'
234
+ Requires-Dist: ruff<0.9,>=0.5; extra == 'develop'
235
+ Requires-Dist: twine<7,>=5; extra == 'develop'
236
+ Provides-Extra: test
237
+ Requires-Dist: pytest; extra == 'test'
238
+ Requires-Dist: pytest-cov; extra == 'test'
239
+ Description-Content-Type: text/markdown
240
+
241
+ # csp slack adapter
242
+
243
+ A [csp](https://github.com/point72/csp) adapter for [slack](https://slack.com)
244
+
245
+ [![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)
246
+ [![GitHub issues](https://img.shields.io/github/issues/point72/csp-adapter-slack.svg)](https://github.com/point72/csp-adapter-slack/issues)
247
+ [![PyPI](https://img.shields.io/pypi/l/csp-adapter-slack.svg)](https://pypi.python.org/pypi/csp-adapter-slack)
248
+ [![PyPI](https://img.shields.io/pypi/v/csp-adapter-slack.svg)](https://pypi.python.org/pypi/csp-adapter-slack)
249
+
250
+ ## Features
251
+
252
+ [More information is available in our wiki](https://github.com/Point72/csp-adapter-slack/wiki)
253
+
254
+ ## Installation
255
+
256
+ Install with `pip`:
257
+
258
+ ```bash
259
+ pip install csp csp-adapter-slack
260
+ ```
261
+
262
+ Install with `conda`
263
+
264
+ ```bash
265
+ conda install csp csp-adapter-slack -c conda-forge
266
+ ```
267
+
268
+ ## License
223
269
 
224
- # csp-adapter-slack
270
+ 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,4 @@
1
+ __version__ = "0.2.0"
2
+
3
+ from .adapter import *
4
+ from .adapter_config import *
@@ -0,0 +1,346 @@
1
+ import threading
2
+ from logging import getLogger
3
+ from queue import Queue
4
+ from threading import Thread
5
+ from time import sleep
6
+ from typing import Dict, List, TypeVar
7
+
8
+ import csp
9
+ from csp.impl.adaptermanager import AdapterManagerImpl
10
+ from csp.impl.outputadapter import OutputAdapter
11
+ from csp.impl.pushadapter import PushInputAdapter
12
+ from csp.impl.struct import Struct
13
+ from csp.impl.types.tstype import ts
14
+ from csp.impl.wiring import py_output_adapter_def, py_push_adapter_def
15
+ from slack_sdk.errors import SlackApiError
16
+ from slack_sdk.socket_mode import SocketModeClient
17
+ from slack_sdk.socket_mode.request import SocketModeRequest
18
+ from slack_sdk.socket_mode.response import SocketModeResponse
19
+ from slack_sdk.web import WebClient
20
+
21
+ from .adapter_config import SlackAdapterConfig
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, config: SlackAdapterConfig):
53
+ self._slack_client = SocketModeClient(
54
+ app_token=config.app_token,
55
+ web_client=WebClient(token=config.bot_token, ssl=config.ssl),
56
+ )
57
+ self._slack_client.socket_mode_request_listeners.append(self._process_slack_message)
58
+
59
+ # down stream edges
60
+ self._subscribers = []
61
+ self._publishers = []
62
+
63
+ # message queues
64
+ self._inqueue: Queue[SlackMessage] = Queue()
65
+ self._outqueue: Queue[SlackMessage] = Queue()
66
+
67
+ # handler thread
68
+ self._running: bool = False
69
+ self._thread: Thread = None
70
+
71
+ # lookups for mentions and redirection
72
+ self._room_id_to_room_name: Dict[str, str] = {}
73
+ self._room_id_to_room_type: Dict[str, str] = {}
74
+ self._room_name_to_room_id: Dict[str, str] = {}
75
+ self._user_id_to_user_name: Dict[str, str] = {}
76
+ self._user_id_to_user_email: Dict[str, str] = {}
77
+ self._user_name_to_user_id: Dict[str, str] = {}
78
+ self._user_email_to_user_id: Dict[str, str] = {}
79
+
80
+ def subscribe(self):
81
+ return _slack_input_adapter(self, push_mode=csp.PushMode.NON_COLLAPSING)
82
+
83
+ def publish(self, msg: ts[SlackMessage]):
84
+ return _slack_output_adapter(self, msg)
85
+
86
+ def _create(self, engine, memo):
87
+ # We'll avoid having a second class and make our AdapterManager and AdapterManagerImpl the same
88
+ super().__init__(engine)
89
+ return self
90
+
91
+ def start(self, starttime, endtime):
92
+ self._running = True
93
+ self._thread = threading.Thread(target=self._run, daemon=True)
94
+ self._thread.start()
95
+
96
+ def stop(self):
97
+ if self._running:
98
+ self._running = False
99
+ self._slack_client.close()
100
+ self._thread.join()
101
+
102
+ def register_subscriber(self, adapter):
103
+ if adapter not in self._subscribers:
104
+ self._subscribers.append(adapter)
105
+
106
+ def register_publisher(self, adapter):
107
+ if adapter not in self._publishers:
108
+ self._publishers.append(adapter)
109
+
110
+ def _get_user_from_id(self, user_id):
111
+ # try to pull from cache
112
+ name = self._user_id_to_user_name.get(user_id, None)
113
+ email = self._user_id_to_user_email.get(user_id, None)
114
+
115
+ # if none, refresh data via web client
116
+ if name is None or email is None:
117
+ ret = self._slack_client.web_client.users_info(user=user_id)
118
+ if ret.status_code == 200:
119
+ # TODO OAuth scopes required
120
+ name = ret.data["user"]["profile"].get("real_name_normalized", ret.data["user"]["name"])
121
+ email = ret.data["user"]["profile"].get("email", "")
122
+ self._user_id_to_user_name[user_id] = name
123
+ self._user_name_to_user_id[name] = user_id # TODO is this 1-1 in slack?
124
+ self._user_id_to_user_email[user_id] = email
125
+ self._user_email_to_user_id[email] = user_id
126
+ return name, email
127
+
128
+ def _get_user_from_name(self, user_name):
129
+ # try to pull from cache
130
+ user_id = self._user_name_to_user_id.get(user_name, None)
131
+
132
+ # if none, refresh data via web client
133
+ if user_id is None:
134
+ # unfortunately the reverse lookup is not super nice...
135
+ # we need to pull all users and build the reverse mapping
136
+ ret = self._slack_client.web_client.users_list()
137
+ if ret.status_code == 200:
138
+ # TODO OAuth scopes required
139
+ for user in ret.data["members"]:
140
+ name = user["profile"].get("real_name_normalized", user["name"])
141
+ user_id = user["profile"]["id"]
142
+ email = user["profile"]["email"]
143
+ self._user_id_to_user_name[user_id] = name
144
+ self._user_name_to_user_id[name] = user_id # TODO is this 1-1 in slack?
145
+ self._user_id_to_user_email[user_id] = email
146
+ self._user_email_to_user_id[email] = user_id
147
+ return self._user_name_to_user_id.get(user_name, None)
148
+ return user_id
149
+
150
+ def _channel_data_to_channel_kind(self, data) -> str:
151
+ if data.get("is_im", False):
152
+ return "message"
153
+ if data.get("is_private", False):
154
+ return "private"
155
+ return "public"
156
+
157
+ def _get_channel_from_id(self, channel_id):
158
+ # try to pull from cache
159
+ name = self._room_id_to_room_name.get(channel_id, None)
160
+ kind = self._room_id_to_room_type.get(channel_id, None)
161
+
162
+ # if none, refresh data via web client
163
+ if name is None:
164
+ ret = self._slack_client.web_client.conversations_info(channel=channel_id)
165
+ if ret.status_code == 200:
166
+ # TODO OAuth scopes required
167
+ kind = self._channel_data_to_channel_kind(ret.data["channel"])
168
+ if kind == "message":
169
+ # TODO use same behavior as symphony adapter
170
+ name = "DM"
171
+ else:
172
+ name = ret.data["channel"]["name"]
173
+
174
+ self._room_id_to_room_name[channel_id] = name
175
+ self._room_name_to_room_id[name] = channel_id
176
+ self._room_id_to_room_type[channel_id] = kind
177
+ return name, kind
178
+
179
+ def _get_channel_from_name(self, channel_name):
180
+ # try to pull from cache
181
+ channel_id = self._room_name_to_room_id.get(channel_name, None)
182
+
183
+ # if none, refresh data via web client
184
+ if channel_id is None:
185
+ # unfortunately the reverse lookup is not super nice...
186
+ # we need to pull all channels and build the reverse mapping
187
+ ret = self._slack_client.web_client.conversations_list()
188
+ if ret.status_code == 200:
189
+ # TODO OAuth scopes required
190
+ for channel in ret.data["channels"]:
191
+ name = channel["name"]
192
+ channel_id = channel["id"]
193
+ kind = self._channel_data_to_channel_kind(channel)
194
+ self._room_id_to_room_name[channel_id] = name
195
+ self._room_name_to_room_id[name] = channel_id
196
+ self._room_id_to_room_type[channel_id] = kind
197
+ return self._room_name_to_room_id.get(channel_name, None)
198
+ return channel_id
199
+
200
+ def _get_tags_from_message(self, blocks) -> List[str]:
201
+ """extract tags from message, potentially excluding the bot's own @"""
202
+ tags = []
203
+ to_search = blocks.copy()
204
+
205
+ while to_search:
206
+ element = to_search.pop()
207
+ # add subsections
208
+ if element.get("elements", []):
209
+ to_search.extend(element.get("elements"))
210
+
211
+ if element.get("type", "") == "user":
212
+ tag_id = element.get("user_id")
213
+ name, _ = self._get_user_from_id(tag_id)
214
+ if name:
215
+ tags.append(name)
216
+ return tags
217
+
218
+ def _process_slack_message(self, client: SocketModeClient, req: SocketModeRequest):
219
+ log.info(req.payload)
220
+ if req.type == "events_api":
221
+ # Acknowledge the request anyway
222
+ response = SocketModeResponse(envelope_id=req.envelope_id)
223
+ client.send_socket_mode_response(response)
224
+
225
+ if req.payload["event"]["type"] in ("message", "app_mention") and req.payload["event"].get("subtype") is None:
226
+ user, user_email = self._get_user_from_id(req.payload["event"]["user"])
227
+ channel, channel_type = self._get_channel_from_id(req.payload["event"]["channel"])
228
+ tags = self._get_tags_from_message(req.payload["event"]["blocks"])
229
+ slack_msg = SlackMessage(
230
+ user=user or "",
231
+ user_email=user_email or "",
232
+ user_id=req.payload["event"]["user"],
233
+ tags=tags,
234
+ channel=channel or "",
235
+ channel_id=req.payload["event"]["channel"],
236
+ channel_type=channel_type or "",
237
+ msg=req.payload["event"]["text"],
238
+ reaction="",
239
+ thread=req.payload["event"]["ts"],
240
+ payload=req.payload.copy(),
241
+ )
242
+ self._inqueue.put(slack_msg)
243
+
244
+ def _run(self):
245
+ self._slack_client.connect()
246
+
247
+ while self._running:
248
+ # drain outbound
249
+ while not self._outqueue.empty():
250
+ # pull SlackMessage from queue
251
+ slack_msg = self._outqueue.get()
252
+
253
+ # refactor into slack command
254
+ # grab channel or DM
255
+ if hasattr(slack_msg, "channel_id") and slack_msg.channel_id:
256
+ channel_id = slack_msg.channel_id
257
+ elif hasattr(slack_msg, "channel") and slack_msg.channel:
258
+ # TODO DM
259
+ channel_id = self._get_channel_from_name(slack_msg.channel)
260
+
261
+ # pull text or reaction
262
+ if hasattr(slack_msg, "reaction") and slack_msg.reaction and hasattr(slack_msg, "thread") and slack_msg.thread:
263
+ # TODO
264
+ self._slack_client.web_client.reactions_add(
265
+ channel=channel_id,
266
+ name=slack_msg.reaction,
267
+ timestamp=slack_msg.thread,
268
+ )
269
+ elif hasattr(slack_msg, "msg") and slack_msg.msg:
270
+ try:
271
+ # send text to channel
272
+ self._slack_client.web_client.chat_postMessage(
273
+ channel=channel_id,
274
+ text=getattr(slack_msg, "msg", ""),
275
+ )
276
+ except SlackApiError:
277
+ # TODO
278
+ ...
279
+ else:
280
+ # cannot send empty message, log an error
281
+ log.error(f"Received malformed SlackMessage instance: {slack_msg}")
282
+
283
+ if not self._inqueue.empty():
284
+ # pull all SlackMessages from queue
285
+ # do as burst to match SymphonyAdapter
286
+ slack_msgs = []
287
+ while not self._inqueue.empty():
288
+ slack_msgs.append(self._inqueue.get())
289
+
290
+ # push to all the subscribers
291
+ for adapter in self._subscribers:
292
+ adapter.push_tick(slack_msgs)
293
+
294
+ # do short sleep
295
+ sleep(0.1)
296
+
297
+ # liveness check
298
+ if not self._thread.is_alive():
299
+ self._running = False
300
+ self._thread.join()
301
+
302
+ # shut down socket client
303
+ try:
304
+ # TODO which one?
305
+ self._slack_client.close()
306
+ # self._slack_client.disconnect()
307
+ except AttributeError:
308
+ # TODO bug in slack sdk causes an exception to be thrown
309
+ # File "slack_sdk/socket_mode/builtin/connection.py", line 191, in disconnect
310
+ # self.sock.close()
311
+ # ^^^^^^^^^^^^^^^
312
+ # AttributeError: 'NoneType' object has no attribute 'close'
313
+ ...
314
+
315
+ def _on_tick(self, value):
316
+ self._outqueue.put(value)
317
+
318
+
319
+ class SlackInputAdapterImpl(PushInputAdapter):
320
+ def __init__(self, manager):
321
+ manager.register_subscriber(self)
322
+ super().__init__()
323
+
324
+
325
+ class SlackOutputAdapterImpl(OutputAdapter):
326
+ def __init__(self, manager):
327
+ manager.register_publisher(self)
328
+ self._manager = manager
329
+ super().__init__()
330
+
331
+ def on_tick(self, time, value):
332
+ self._manager._on_tick(value)
333
+
334
+
335
+ _slack_input_adapter = py_push_adapter_def(
336
+ name="SlackInputAdapter",
337
+ adapterimpl=SlackInputAdapterImpl,
338
+ out_type=ts[[SlackMessage]],
339
+ manager_type=SlackAdapterManager,
340
+ )
341
+ _slack_output_adapter = py_output_adapter_def(
342
+ name="SlackOutputAdapter",
343
+ adapterimpl=SlackOutputAdapterImpl,
344
+ manager_type=SlackAdapterManager,
345
+ input=ts[SlackMessage],
346
+ )
@@ -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,213 @@
1
+ from datetime import timedelta
2
+ from ssl import create_default_context
3
+ from unittest.mock import MagicMock, call, patch
4
+
5
+ import csp
6
+ import pytest
7
+ from csp import ts
8
+ from pydantic import ValidationError
9
+
10
+ from csp_adapter_slack import SlackAdapterConfig, SlackAdapterManager, SlackMessage, mention_user
11
+
12
+
13
+ @csp.node
14
+ def hello(msg: ts[SlackMessage]) -> ts[SlackMessage]:
15
+ if csp.ticked(msg):
16
+ text = f"Hello <@{msg.user_id}>!"
17
+ return SlackMessage(
18
+ channel="a new channel",
19
+ # reply in thread
20
+ thread=msg.thread,
21
+ msg=text,
22
+ )
23
+
24
+
25
+ @csp.node
26
+ def react(msg: ts[SlackMessage]) -> ts[SlackMessage]:
27
+ if csp.ticked(msg):
28
+ return SlackMessage(
29
+ channel=msg.channel,
30
+ channel_id=msg.channel_id,
31
+ thread=msg.thread,
32
+ reaction="eyes",
33
+ )
34
+
35
+
36
+ @csp.node
37
+ def send_fake_message(clientmock: MagicMock, requestmock: MagicMock, am: SlackAdapterManager) -> ts[bool]:
38
+ with csp.alarms():
39
+ a_send = csp.alarm(bool)
40
+ with csp.start():
41
+ csp.schedule_alarm(a_send, timedelta(seconds=1), True)
42
+ if csp.ticked(a_send):
43
+ if a_send:
44
+ am._process_slack_message(clientmock, requestmock)
45
+ csp.schedule_alarm(a_send, timedelta(seconds=1), False)
46
+ else:
47
+ return True
48
+
49
+
50
+ PUBLIC_CHANNEL_MENTION_PAYLOAD = {
51
+ "token": "ABCD",
52
+ "team_id": "EFGH",
53
+ "api_app_id": "HIJK",
54
+ "event": {
55
+ "client_msg_id": "1234-5678",
56
+ "type": "app_mention",
57
+ "text": "<@BOTID> <@USERID> <@USERID2>",
58
+ "user": "USERID",
59
+ "ts": "1.2",
60
+ "blocks": [
61
+ {
62
+ "type": "rich_text",
63
+ "block_id": "tx381",
64
+ "elements": [
65
+ {
66
+ "type": "rich_text_section",
67
+ "elements": [
68
+ {"type": "user", "user_id": "BOTID"},
69
+ {"type": "text", "text": " "},
70
+ {"type": "user", "user_id": "USERID"},
71
+ {"type": "text", "text": " "},
72
+ {"type": "user", "user_id": "USERID2"},
73
+ ],
74
+ }
75
+ ],
76
+ }
77
+ ],
78
+ "team": "ABCD",
79
+ "channel": "EFGH",
80
+ "event_ts": "1.2",
81
+ },
82
+ "type": "event_callback",
83
+ "event_id": "ABCD",
84
+ "event_time": 1707423091,
85
+ "authorizations": [{"enterprise_id": None, "team_id": "ABCD", "user_id": "BOTID", "is_bot": True, "is_enterprise_install": False}],
86
+ "is_ext_shared_channel": False,
87
+ "event_context": "SOMELONGCONTEXT",
88
+ }
89
+ DIRECT_MESSAGE_PAYLOAD = {
90
+ "token": "ABCD",
91
+ "team_id": "EFGH",
92
+ "context_team_id": "ABCD",
93
+ "context_enterprise_id": None,
94
+ "api_app_id": "HIJK",
95
+ "event": {
96
+ "client_msg_id": "1234-5678",
97
+ "type": "message",
98
+ "text": "test",
99
+ "user": "USERID",
100
+ "ts": "2.1",
101
+ "blocks": [
102
+ {
103
+ "type": "rich_text",
104
+ "block_id": "gB9fq",
105
+ "elements": [{"type": "rich_text_section", "elements": [{"type": "text", "text": "test"}]}],
106
+ }
107
+ ],
108
+ "team": "ABCD",
109
+ "channel": "EFGH",
110
+ "event_ts": "2.1",
111
+ "channel_type": "im",
112
+ },
113
+ "type": "event_callback",
114
+ "event_id": "ABCD",
115
+ "event_time": 1707423220,
116
+ "authorizations": [{"enterprise_id": None, "team_id": "ABCD", "user_id": "BOTID", "is_bot": True, "is_enterprise_install": False}],
117
+ "is_ext_shared_channel": False,
118
+ "event_context": "SOMELONGCONTEXT",
119
+ }
120
+
121
+
122
+ class TestSlack:
123
+ def test_slack_tokens(self):
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")
128
+
129
+ @pytest.mark.parametrize("payload", (PUBLIC_CHANNEL_MENTION_PAYLOAD, DIRECT_MESSAGE_PAYLOAD))
130
+ def test_slack(self, payload):
131
+ with patch("csp_adapter_slack.adapter.SocketModeClient") as clientmock:
132
+ # mock out the event from the slack sdk
133
+ reqmock = MagicMock()
134
+ reqmock.type = "events_api"
135
+ reqmock.payload = payload
136
+
137
+ # mock out the user/room lookup responses
138
+ mock_user_response = MagicMock(name="users_info_mock")
139
+ mock_user_response.status_code = 200
140
+ mock_user_response.data = {"user": {"profile": {"real_name_normalized": "johndoe", "email": "johndoe@some.email"}, "name": "blerg"}}
141
+ clientmock.return_value.web_client.users_info.return_value = mock_user_response
142
+ mock_room_response = MagicMock(name="conversations_info_mock")
143
+ mock_room_response.status_code = 200
144
+ mock_room_response.data = {"channel": {"is_im": False, "is_private": True, "name": "a private channel"}}
145
+ clientmock.return_value.web_client.conversations_info.return_value = mock_room_response
146
+ mock_list_response = MagicMock(name="conversations_list_mock")
147
+ mock_list_response.status_code = 200
148
+ mock_list_response.data = {
149
+ "channels": [
150
+ {"name": "a private channel", "id": "EFGH"},
151
+ {"name": "a new channel", "id": "new_channel"},
152
+ ]
153
+ }
154
+ clientmock.return_value.web_client.conversations_list.return_value = mock_list_response
155
+
156
+ def graph():
157
+ am = SlackAdapterManager(SlackAdapterConfig(app_token="xapp-1-dummy", bot_token="xoxb-dummy", ssl=create_default_context()))
158
+
159
+ # send a fake slack message to the app
160
+ stop = send_fake_message(clientmock, reqmock, am)
161
+
162
+ # send a response
163
+ resp = hello(csp.unroll(am.subscribe()))
164
+ am.publish(resp)
165
+
166
+ # do a react
167
+ rct = react(csp.unroll(am.subscribe()))
168
+ am.publish(rct)
169
+
170
+ csp.add_graph_output("response", resp)
171
+ csp.add_graph_output("react", rct)
172
+
173
+ # stop after first messages
174
+ done_flag = (csp.count(stop) + csp.count(resp) + csp.count(rct)) == 3
175
+ csp.stop_engine(done_flag)
176
+
177
+ # run the graph
178
+ resp = csp.run(graph, realtime=True)
179
+
180
+ # check outputs
181
+ if payload == PUBLIC_CHANNEL_MENTION_PAYLOAD:
182
+ assert resp["react"]
183
+ assert resp["response"]
184
+
185
+ assert resp["react"][0][1] == SlackMessage(channel="a private channel", channel_id="EFGH", reaction="eyes", thread="1.2")
186
+ assert resp["response"][0][1] == SlackMessage(channel="a new channel", msg="Hello <@USERID>!", thread="1.2")
187
+ else:
188
+ assert resp["react"]
189
+ assert resp["response"]
190
+
191
+ assert resp["react"][0][1] == SlackMessage(channel="a private channel", channel_id="EFGH", reaction="eyes", thread="2.1")
192
+ assert resp["response"][0][1] == SlackMessage(channel="a new channel", msg="Hello <@USERID>!", thread="2.1")
193
+
194
+ # check all inbound mocks got called
195
+ if payload == PUBLIC_CHANNEL_MENTION_PAYLOAD:
196
+ assert clientmock.return_value.web_client.users_info.call_count == 2
197
+ else:
198
+ assert clientmock.return_value.web_client.users_info.call_count == 1
199
+ assert clientmock.return_value.web_client.conversations_info.call_count == 1
200
+
201
+ # check all outbound mocks got called
202
+ assert clientmock.return_value.web_client.reactions_add.call_count == 1
203
+ assert clientmock.return_value.web_client.chat_postMessage.call_count == 1
204
+
205
+ if payload == PUBLIC_CHANNEL_MENTION_PAYLOAD:
206
+ assert clientmock.return_value.web_client.reactions_add.call_args_list == [call(channel="EFGH", name="eyes", timestamp="1.2")]
207
+ assert clientmock.return_value.web_client.chat_postMessage.call_args_list == [call(channel="new_channel", text="Hello <@USERID>!")]
208
+ else:
209
+ assert clientmock.return_value.web_client.reactions_add.call_args_list == [call(channel="EFGH", name="eyes", timestamp="2.1")]
210
+ assert clientmock.return_value.web_client.chat_postMessage.call_args_list == [call(channel="new_channel", text="Hello <@USERID>!")]
211
+
212
+ def test_mention_user(self):
213
+ assert mention_user("ABCD") == "<@ABCD>"
@@ -0,0 +1,144 @@
1
+ [build-system]
2
+ requires = [
3
+ "hatchling>=1.22.4,<1.27",
4
+ "pkginfo>=1.10,<1.12",
5
+ ]
6
+ build-backend = "hatchling.build"
7
+
8
+ [project]
9
+ name = "csp_adapter_slack"
10
+ description = "A csp adapter for slack"
11
+ version = "0.2.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
+ "Programming Language :: Python :: 3.13",
34
+ "License :: OSI Approved :: Apache Software License",
35
+ ]
36
+ dependencies = [
37
+ "csp",
38
+ "pydantic>=2",
39
+ "slack-sdk>=3",
40
+ ]
41
+
42
+ [project.optional-dependencies]
43
+ develop = [
44
+ "bump-my-version",
45
+ "check-manifest",
46
+ "codespell>=2.2.6,<2.4",
47
+ "hatchling",
48
+ "mdformat>=0.7.17,<0.8",
49
+ "mdformat-tables>=1,<1.1",
50
+ "ruff>=0.5,<0.9",
51
+ "twine>=5,<7",
52
+ # test
53
+ "pytest",
54
+ "pytest-cov",
55
+ ]
56
+ test = [
57
+ "pytest",
58
+ "pytest-cov",
59
+ ]
60
+
61
+ [project.urls]
62
+ Repository = "https://github.com/point72/csp-adapter-slack"
63
+ Homepage = "https://github.com/point72/csp-adapter-slack"
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
+
81
+ [tool.check-manifest]
82
+ ignore = []
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
+
97
+ [tool.hatch.build]
98
+ artifacts = []
99
+
100
+ [tool.hatch.build.sources]
101
+ src = "/"
102
+
103
+ [tool.hatch.build.targets.sdist]
104
+ packages = ["csp_adapter_slack"]
105
+ exclude = [
106
+ "/.github",
107
+ "/.gitignore",
108
+ "/docs",
109
+ ]
110
+
111
+ [tool.hatch.build.targets.wheel]
112
+ packages = ["csp_adapter_slack"]
113
+ exclude = [
114
+ "/.github",
115
+ "/.gitignore",
116
+ "/pyproject.toml",
117
+ "/docs",
118
+ ]
119
+
120
+ [tool.pytest.ini_options]
121
+ addopts = ["-vvv", "--junitxml=junit.xml"]
122
+ asyncio_mode = "strict"
123
+ testpaths = "csp_adapter_slack/tests"
124
+
125
+ [tool.ruff]
126
+ line-length = 150
127
+
128
+ [tool.ruff.lint]
129
+ extend-select = ["I"]
130
+
131
+ [tool.ruff.lint.per-file-ignores]
132
+ "__init__.py" = ["F401", "F403"]
133
+
134
+ [tool.ruff.lint.isort]
135
+ combine-as-imports = true
136
+ default-section = "third-party"
137
+ known-first-party = ["csp_adapter_slack"]
138
+ section-order = [
139
+ "future",
140
+ "standard-library",
141
+ "third-party",
142
+ "first-party",
143
+ "local-folder",
144
+ ]
@@ -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()