ml-analytics-tools 0.2.0__py3-none-any.whl
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.
- ml_analytics/__init__.py +53 -0
- ml_analytics/aws_auth.py +169 -0
- ml_analytics/cli.py +58 -0
- ml_analytics/data_connector.py +2615 -0
- ml_analytics/gsheet_connector.py +1646 -0
- ml_analytics/model_manager.py +1208 -0
- ml_analytics/model_tools.py +990 -0
- ml_analytics/s3_connector.py +1381 -0
- ml_analytics/slack_connector.py +637 -0
- ml_analytics/tunnel_manager.py +277 -0
- ml_analytics/utils.py +673 -0
- ml_analytics_tools-0.2.0.dist-info/METADATA +231 -0
- ml_analytics_tools-0.2.0.dist-info/RECORD +17 -0
- ml_analytics_tools-0.2.0.dist-info/WHEEL +5 -0
- ml_analytics_tools-0.2.0.dist-info/entry_points.txt +4 -0
- ml_analytics_tools-0.2.0.dist-info/licenses/LICENSE +21 -0
- ml_analytics_tools-0.2.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,637 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Slack connector for sending messages, images, and files to Slack channels and users.
|
|
3
|
+
|
|
4
|
+
This module provides a simple interface to interact with Slack's API for common tasks
|
|
5
|
+
such as sending messages, uploading files, and managing channel communications.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
from slack_sdk import WebClient
|
|
11
|
+
from slack_sdk.errors import SlackApiError
|
|
12
|
+
|
|
13
|
+
from .utils import get_credential_value, get_logger, log_and_raise_error
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class SlackConnector:
|
|
17
|
+
"""
|
|
18
|
+
A connector class for interacting with Slack API.
|
|
19
|
+
|
|
20
|
+
This class provides methods to send messages, upload files, and interact
|
|
21
|
+
with Slack channels using a bot token.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
def __init__(
|
|
25
|
+
self,
|
|
26
|
+
token: str = None,
|
|
27
|
+
token_path: str | Path = None,
|
|
28
|
+
log_level: str = "INFO",
|
|
29
|
+
return_response: bool = False,
|
|
30
|
+
scope: str = "ml",
|
|
31
|
+
):
|
|
32
|
+
"""
|
|
33
|
+
Initialize the Slack connector.
|
|
34
|
+
|
|
35
|
+
Parameters
|
|
36
|
+
----------
|
|
37
|
+
token : str, optional
|
|
38
|
+
Slack Bot Token (starts with 'xoxb-').
|
|
39
|
+
If not provided, will check SLACK_BOT_TOKEN environment variable or mounted secrets,
|
|
40
|
+
then token_path, then default locations.
|
|
41
|
+
token_path : str | Path, optional
|
|
42
|
+
Path to file containing the Slack token.
|
|
43
|
+
If not provided, will look for 'slack_token.txt' in current directory.
|
|
44
|
+
log_level : str, optional
|
|
45
|
+
Logging level. Default is "INFO".
|
|
46
|
+
return_response : bool, optional
|
|
47
|
+
If True, methods return full API response. If False, returns None for cleaner output.
|
|
48
|
+
Default is False.
|
|
49
|
+
scope : str, optional
|
|
50
|
+
Scope for mounted secrets (e.g., '/mnt/{scope}/SLACK_BOT_TOKEN').
|
|
51
|
+
Default is "ml".
|
|
52
|
+
|
|
53
|
+
Examples
|
|
54
|
+
--------
|
|
55
|
+
>>> # Using token directly
|
|
56
|
+
>>> slack = SlackConnector(token="xoxb-your-token")
|
|
57
|
+
>>>
|
|
58
|
+
>>> # Using token from .env file (recommended)
|
|
59
|
+
>>> # Add to .env: SLACK_BOT_TOKEN=xoxb-your-token
|
|
60
|
+
>>> slack = SlackConnector()
|
|
61
|
+
>>>
|
|
62
|
+
>>> # Using token file
|
|
63
|
+
>>> slack = SlackConnector(token_path="slack_token.txt")
|
|
64
|
+
>>>
|
|
65
|
+
>>> # Using mounted secrets with a custom scope
|
|
66
|
+
>>> slack = SlackConnector(scope="custom-scope")
|
|
67
|
+
"""
|
|
68
|
+
self._logger = get_logger("SlackConnector")
|
|
69
|
+
self._logger.setLevel(log_level)
|
|
70
|
+
self.return_response = return_response
|
|
71
|
+
|
|
72
|
+
if token is None and token_path is None:
|
|
73
|
+
# Try to get token from environment variable or mounted secret
|
|
74
|
+
try:
|
|
75
|
+
token = get_credential_value("SLACK_BOT_TOKEN", scope=scope)
|
|
76
|
+
self._logger.debug("Using Slack token from environment or mounted secret")
|
|
77
|
+
except Exception:
|
|
78
|
+
# Fall back to checking default file location
|
|
79
|
+
default_path = Path.cwd() / "slack_token.txt"
|
|
80
|
+
if default_path.exists():
|
|
81
|
+
token_path = default_path
|
|
82
|
+
else:
|
|
83
|
+
log_and_raise_error(
|
|
84
|
+
self._logger,
|
|
85
|
+
"Slack token not found. Please provide one of:\n"
|
|
86
|
+
" 1. Set SLACK_BOT_TOKEN in .env file\n"
|
|
87
|
+
" 2. Pass 'token' parameter\n"
|
|
88
|
+
" 3. Pass 'token_path' parameter\n"
|
|
89
|
+
" 4. Create 'slack_token.txt' in current directory\n"
|
|
90
|
+
" 5. Mount secret at /mnt/<scope>/SLACK_BOT_TOKEN",
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
if token_path is not None:
|
|
94
|
+
token_path = Path(token_path)
|
|
95
|
+
if not token_path.exists():
|
|
96
|
+
log_and_raise_error(
|
|
97
|
+
self._logger,
|
|
98
|
+
f"Token file not found at: {token_path}",
|
|
99
|
+
)
|
|
100
|
+
token = token_path.read_text().strip()
|
|
101
|
+
|
|
102
|
+
try:
|
|
103
|
+
self.client = WebClient(token=token)
|
|
104
|
+
response = self.client.auth_test()
|
|
105
|
+
self.bot_user_id = response["user_id"]
|
|
106
|
+
self.team_name = response.get("team", "Unknown")
|
|
107
|
+
self.bot_name = response.get("user", "Bot")
|
|
108
|
+
self._logger.info(f"Successfully connected to Slack workspace: {self.team_name} (bot: {self.bot_name})")
|
|
109
|
+
except SlackApiError as e:
|
|
110
|
+
log_and_raise_error(
|
|
111
|
+
self._logger,
|
|
112
|
+
f"Failed to connect to Slack: {e.response['error']}",
|
|
113
|
+
)
|
|
114
|
+
except Exception as e:
|
|
115
|
+
log_and_raise_error(
|
|
116
|
+
self._logger,
|
|
117
|
+
f"Error initializing Slack client: {e}",
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
def send_message(
|
|
121
|
+
self,
|
|
122
|
+
channel: str,
|
|
123
|
+
text: str,
|
|
124
|
+
blocks: list[dict] = None,
|
|
125
|
+
thread_ts: str = None,
|
|
126
|
+
unfurl_links: bool = True,
|
|
127
|
+
unfurl_media: bool = True,
|
|
128
|
+
) -> dict:
|
|
129
|
+
"""
|
|
130
|
+
Send a message to a Slack channel or user.
|
|
131
|
+
|
|
132
|
+
Parameters
|
|
133
|
+
----------
|
|
134
|
+
channel : str
|
|
135
|
+
Channel name (e.g., '#general', 'general') or user ID.
|
|
136
|
+
text : str
|
|
137
|
+
Message text (also used as fallback for notifications).
|
|
138
|
+
blocks : list[dict], optional
|
|
139
|
+
Slack Block Kit blocks for rich formatting.
|
|
140
|
+
thread_ts : str, optional
|
|
141
|
+
Thread timestamp to reply in a thread.
|
|
142
|
+
unfurl_links : bool, optional
|
|
143
|
+
Whether to unfurl links. Default is True.
|
|
144
|
+
unfurl_media : bool, optional
|
|
145
|
+
Whether to unfurl media. Default is True.
|
|
146
|
+
|
|
147
|
+
Returns
|
|
148
|
+
-------
|
|
149
|
+
dict
|
|
150
|
+
API response containing message details.
|
|
151
|
+
|
|
152
|
+
Examples
|
|
153
|
+
--------
|
|
154
|
+
>>> slack = SlackConnector(token="xoxb-your-token")
|
|
155
|
+
>>>
|
|
156
|
+
>>> # Simple message
|
|
157
|
+
>>> slack.send_message("#general", "Hello from Python!")
|
|
158
|
+
>>>
|
|
159
|
+
>>> # Rich formatting with blocks
|
|
160
|
+
>>> blocks = [
|
|
161
|
+
... {
|
|
162
|
+
... "type": "section",
|
|
163
|
+
... "text": {"type": "mrkdwn", "text": "*Important Alert*"}
|
|
164
|
+
... }
|
|
165
|
+
... ]
|
|
166
|
+
>>> slack.send_message("#alerts", "Alert", blocks=blocks)
|
|
167
|
+
>>>
|
|
168
|
+
>>> # Reply in a thread
|
|
169
|
+
>>> response = slack.send_message("#general", "Main message")
|
|
170
|
+
>>> slack.send_message("#general", "Reply", thread_ts=response["ts"])
|
|
171
|
+
"""
|
|
172
|
+
# Resolve channel (accepts name or ID)
|
|
173
|
+
channel_id = self.get_or_resolve_channel_id(channel)
|
|
174
|
+
|
|
175
|
+
try:
|
|
176
|
+
response = self.client.chat_postMessage(
|
|
177
|
+
channel=channel_id,
|
|
178
|
+
text=text,
|
|
179
|
+
blocks=blocks,
|
|
180
|
+
thread_ts=thread_ts,
|
|
181
|
+
unfurl_links=unfurl_links,
|
|
182
|
+
unfurl_media=unfurl_media,
|
|
183
|
+
)
|
|
184
|
+
self._logger.info("Message sent successfully")
|
|
185
|
+
return response.data if self.return_response else None
|
|
186
|
+
|
|
187
|
+
except SlackApiError as e:
|
|
188
|
+
log_and_raise_error(
|
|
189
|
+
self._logger,
|
|
190
|
+
f"Error sending message to Slack: {e.response['error']}",
|
|
191
|
+
)
|
|
192
|
+
except Exception as e:
|
|
193
|
+
log_and_raise_error(
|
|
194
|
+
self._logger,
|
|
195
|
+
f"Error sending message: {e}",
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
def send_message_with_image(
|
|
199
|
+
self,
|
|
200
|
+
channel: str,
|
|
201
|
+
text: str,
|
|
202
|
+
image_url: str,
|
|
203
|
+
image_alt: str = "image",
|
|
204
|
+
title: str = None,
|
|
205
|
+
thread_ts: str = None,
|
|
206
|
+
) -> dict:
|
|
207
|
+
"""
|
|
208
|
+
Send a message with an inline image using Block Kit.
|
|
209
|
+
|
|
210
|
+
Note: The image URL must be publicly accessible.
|
|
211
|
+
|
|
212
|
+
Parameters
|
|
213
|
+
----------
|
|
214
|
+
channel : str
|
|
215
|
+
Channel name or ID.
|
|
216
|
+
text : str
|
|
217
|
+
Message text.
|
|
218
|
+
image_url : str
|
|
219
|
+
Publicly accessible URL to the image.
|
|
220
|
+
image_alt : str, optional
|
|
221
|
+
Alt text for the image. Default is "image".
|
|
222
|
+
title : str, optional
|
|
223
|
+
Optional title above the image.
|
|
224
|
+
thread_ts : str, optional
|
|
225
|
+
Thread timestamp to reply in a thread.
|
|
226
|
+
|
|
227
|
+
Returns
|
|
228
|
+
-------
|
|
229
|
+
dict
|
|
230
|
+
API response containing message details.
|
|
231
|
+
|
|
232
|
+
Examples
|
|
233
|
+
--------
|
|
234
|
+
>>> slack = SlackConnector(token="xoxb-your-token")
|
|
235
|
+
>>> slack.send_message_with_image(
|
|
236
|
+
... "#general",
|
|
237
|
+
... "Check out our results!",
|
|
238
|
+
... "https://example.com/chart.png",
|
|
239
|
+
... image_alt="Performance chart",
|
|
240
|
+
... title="Model Performance"
|
|
241
|
+
... )
|
|
242
|
+
"""
|
|
243
|
+
blocks = []
|
|
244
|
+
|
|
245
|
+
if title:
|
|
246
|
+
blocks.append({"type": "section", "text": {"type": "mrkdwn", "text": f"*{title}*"}})
|
|
247
|
+
|
|
248
|
+
blocks.extend(
|
|
249
|
+
[
|
|
250
|
+
{"type": "section", "text": {"type": "mrkdwn", "text": text}},
|
|
251
|
+
{"type": "image", "image_url": image_url, "alt_text": image_alt},
|
|
252
|
+
]
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
result = self.send_message(channel, text, blocks=blocks, thread_ts=thread_ts)
|
|
256
|
+
self._logger.info("Message with image sent successfully")
|
|
257
|
+
return result
|
|
258
|
+
|
|
259
|
+
def send_file(
|
|
260
|
+
self,
|
|
261
|
+
channels: str | list[str],
|
|
262
|
+
file_path: str | Path = None,
|
|
263
|
+
content: str | bytes = None,
|
|
264
|
+
filename: str = None,
|
|
265
|
+
title: str = None,
|
|
266
|
+
initial_comment: str = None,
|
|
267
|
+
thread_ts: str = None,
|
|
268
|
+
) -> dict:
|
|
269
|
+
"""
|
|
270
|
+
Upload a file to Slack channel(s).
|
|
271
|
+
|
|
272
|
+
Parameters
|
|
273
|
+
----------
|
|
274
|
+
channels : str | list[str]
|
|
275
|
+
Channel name(s) to upload to (e.g., '#general', 'general').
|
|
276
|
+
file_path : str | Path, optional
|
|
277
|
+
Path to file to upload.
|
|
278
|
+
content : str | bytes, optional
|
|
279
|
+
File content (alternative to file_path).
|
|
280
|
+
filename : str, optional
|
|
281
|
+
Filename to display in Slack (required if using content).
|
|
282
|
+
title : str, optional
|
|
283
|
+
Title of the file.
|
|
284
|
+
initial_comment : str, optional
|
|
285
|
+
Comment to add with the file.
|
|
286
|
+
thread_ts : str, optional
|
|
287
|
+
Thread timestamp to upload file to a thread.
|
|
288
|
+
|
|
289
|
+
Returns
|
|
290
|
+
-------
|
|
291
|
+
dict
|
|
292
|
+
API response containing file details.
|
|
293
|
+
|
|
294
|
+
Examples
|
|
295
|
+
--------
|
|
296
|
+
>>> slack = SlackConnector(token="xoxb-your-token")
|
|
297
|
+
>>>
|
|
298
|
+
>>> # Upload a file
|
|
299
|
+
>>> slack.send_file(
|
|
300
|
+
... channels="#data-team",
|
|
301
|
+
... file_path="report.csv",
|
|
302
|
+
... title="Daily Report",
|
|
303
|
+
... initial_comment="Here's today's report"
|
|
304
|
+
... )
|
|
305
|
+
>>>
|
|
306
|
+
>>> # Upload to multiple channels
|
|
307
|
+
>>> slack.send_file(
|
|
308
|
+
... channels=["#team-a", "#team-b"],
|
|
309
|
+
... file_path="metrics.png",
|
|
310
|
+
... title="Metrics Dashboard"
|
|
311
|
+
... )
|
|
312
|
+
>>>
|
|
313
|
+
>>> # Upload from content
|
|
314
|
+
>>> import pandas as pd
|
|
315
|
+
>>> df = pd.DataFrame({'col1': [1, 2, 3]})
|
|
316
|
+
>>> csv_content = df.to_csv(index=False)
|
|
317
|
+
>>> slack.send_file(
|
|
318
|
+
... channels="#data-team",
|
|
319
|
+
... content=csv_content,
|
|
320
|
+
... filename="data.csv",
|
|
321
|
+
... title="Generated Data"
|
|
322
|
+
... )
|
|
323
|
+
"""
|
|
324
|
+
|
|
325
|
+
if isinstance(channels, str):
|
|
326
|
+
channels = [channels]
|
|
327
|
+
|
|
328
|
+
channel_ids = [self.get_or_resolve_channel_id(ch) for ch in channels]
|
|
329
|
+
|
|
330
|
+
try:
|
|
331
|
+
if file_path is not None:
|
|
332
|
+
response = self.client.files_upload_v2(
|
|
333
|
+
channel=channel_ids[0] if len(channel_ids) == 1 else None,
|
|
334
|
+
channels=channel_ids if len(channel_ids) > 1 else None,
|
|
335
|
+
file=str(file_path),
|
|
336
|
+
title=title,
|
|
337
|
+
initial_comment=initial_comment,
|
|
338
|
+
thread_ts=thread_ts,
|
|
339
|
+
)
|
|
340
|
+
elif content is not None:
|
|
341
|
+
if filename is None:
|
|
342
|
+
log_and_raise_error(
|
|
343
|
+
self._logger,
|
|
344
|
+
"Parameter 'filename' is required when using 'content'",
|
|
345
|
+
)
|
|
346
|
+
response = self.client.files_upload_v2(
|
|
347
|
+
channel=channel_ids[0] if len(channel_ids) == 1 else None,
|
|
348
|
+
channels=channel_ids if len(channel_ids) > 1 else None,
|
|
349
|
+
content=content,
|
|
350
|
+
filename=filename,
|
|
351
|
+
title=title,
|
|
352
|
+
initial_comment=initial_comment,
|
|
353
|
+
thread_ts=thread_ts,
|
|
354
|
+
)
|
|
355
|
+
else:
|
|
356
|
+
log_and_raise_error(
|
|
357
|
+
self._logger,
|
|
358
|
+
"Either 'file_path' or 'content' must be provided",
|
|
359
|
+
)
|
|
360
|
+
|
|
361
|
+
channel_list = ", ".join([ch.lstrip("#") for ch in channels])
|
|
362
|
+
self._logger.info(f"Successfully uploaded file to {channel_list}")
|
|
363
|
+
return response.data if self.return_response else None
|
|
364
|
+
|
|
365
|
+
except SlackApiError as e:
|
|
366
|
+
log_and_raise_error(
|
|
367
|
+
self._logger,
|
|
368
|
+
f"Error uploading file to Slack: {e.response['error']}",
|
|
369
|
+
)
|
|
370
|
+
except Exception as e:
|
|
371
|
+
log_and_raise_error(
|
|
372
|
+
self._logger,
|
|
373
|
+
f"Error uploading file: {e}",
|
|
374
|
+
)
|
|
375
|
+
|
|
376
|
+
def list_channels(
|
|
377
|
+
self,
|
|
378
|
+
types: str = "public_channel",
|
|
379
|
+
limit: int = 1000,
|
|
380
|
+
exclude_archived: bool = True,
|
|
381
|
+
) -> list[dict]:
|
|
382
|
+
"""
|
|
383
|
+
List channels in the workspace.
|
|
384
|
+
|
|
385
|
+
Parameters
|
|
386
|
+
----------
|
|
387
|
+
types : str, optional
|
|
388
|
+
Comma-separated channel types to list.
|
|
389
|
+
Options: 'public_channel', 'private_channel', 'im', 'mpim'.
|
|
390
|
+
Default is 'public_channel'.
|
|
391
|
+
limit : int, optional
|
|
392
|
+
Maximum number of channels to return. Default is 1000.
|
|
393
|
+
exclude_archived : bool, optional
|
|
394
|
+
Whether to exclude archived channels. Default is True.
|
|
395
|
+
|
|
396
|
+
Returns
|
|
397
|
+
-------
|
|
398
|
+
list[dict]
|
|
399
|
+
List of channel objects with keys: id, name, is_channel, is_private, etc.
|
|
400
|
+
|
|
401
|
+
Examples
|
|
402
|
+
--------
|
|
403
|
+
>>> slack = SlackConnector(token="xoxb-your-token")
|
|
404
|
+
>>>
|
|
405
|
+
>>> # List public channels
|
|
406
|
+
>>> channels = slack.list_channels()
|
|
407
|
+
>>> for ch in channels:
|
|
408
|
+
... print(f"#{ch['name']}")
|
|
409
|
+
>>>
|
|
410
|
+
>>> # List private channels (requires appropriate scopes)
|
|
411
|
+
>>> private = slack.list_channels(types="private_channel")
|
|
412
|
+
"""
|
|
413
|
+
try:
|
|
414
|
+
response = self.client.conversations_list(
|
|
415
|
+
types=types,
|
|
416
|
+
limit=limit,
|
|
417
|
+
exclude_archived=exclude_archived,
|
|
418
|
+
)
|
|
419
|
+
|
|
420
|
+
return response["channels"]
|
|
421
|
+
|
|
422
|
+
except SlackApiError as e:
|
|
423
|
+
log_and_raise_error(
|
|
424
|
+
self._logger,
|
|
425
|
+
f"Error listing channels: {e.response['error']}",
|
|
426
|
+
)
|
|
427
|
+
except Exception as e:
|
|
428
|
+
log_and_raise_error(
|
|
429
|
+
self._logger,
|
|
430
|
+
f"Error listing channels: {e}",
|
|
431
|
+
)
|
|
432
|
+
|
|
433
|
+
def get_channel_id(self, channel_name: str) -> str | None:
|
|
434
|
+
"""
|
|
435
|
+
Get channel ID from channel name.
|
|
436
|
+
|
|
437
|
+
Parameters
|
|
438
|
+
----------
|
|
439
|
+
channel_name : str
|
|
440
|
+
Channel name (with or without #).
|
|
441
|
+
|
|
442
|
+
Returns
|
|
443
|
+
-------
|
|
444
|
+
str | None
|
|
445
|
+
Channel ID if found, None otherwise.
|
|
446
|
+
|
|
447
|
+
Examples
|
|
448
|
+
--------
|
|
449
|
+
>>> slack = SlackConnector(token="xoxb-your-token")
|
|
450
|
+
>>> channel_id = slack.get_channel_id("#general")
|
|
451
|
+
>>> print(channel_id) # C01234567
|
|
452
|
+
"""
|
|
453
|
+
# Clean channel name
|
|
454
|
+
if channel_name.startswith("#"):
|
|
455
|
+
channel_name = channel_name[1:]
|
|
456
|
+
|
|
457
|
+
try:
|
|
458
|
+
# Try private channels first
|
|
459
|
+
try:
|
|
460
|
+
private_channels = self.list_channels(types="private_channel")
|
|
461
|
+
for channel in private_channels:
|
|
462
|
+
if channel["name"] == channel_name:
|
|
463
|
+
return channel["id"]
|
|
464
|
+
except Exception:
|
|
465
|
+
pass
|
|
466
|
+
|
|
467
|
+
channels = self.list_channels(types="public_channel")
|
|
468
|
+
for channel in channels:
|
|
469
|
+
if channel["name"] == channel_name:
|
|
470
|
+
return channel["id"]
|
|
471
|
+
|
|
472
|
+
return None
|
|
473
|
+
|
|
474
|
+
except Exception as e:
|
|
475
|
+
self._logger.error(f"Error getting channel ID: {e}")
|
|
476
|
+
return None
|
|
477
|
+
|
|
478
|
+
def get_or_resolve_channel_id(self, channel: str) -> str:
|
|
479
|
+
"""
|
|
480
|
+
Get channel ID by name. If not found in conversations.list,
|
|
481
|
+
send a test message to resolve the ID (works for Slack Connect / multi-workspace).
|
|
482
|
+
"""
|
|
483
|
+
|
|
484
|
+
channel_clean = channel.lstrip("#")
|
|
485
|
+
channel_id = self.get_channel_id(channel_clean)
|
|
486
|
+
if channel_id:
|
|
487
|
+
return channel_id
|
|
488
|
+
|
|
489
|
+
try:
|
|
490
|
+
resp = self.client.chat_postMessage(channel=channel_clean, text=".")
|
|
491
|
+
resolved_id = resp["channel"]
|
|
492
|
+
|
|
493
|
+
try:
|
|
494
|
+
self.client.chat_delete(channel=resolved_id, ts=resp["ts"])
|
|
495
|
+
except Exception:
|
|
496
|
+
pass
|
|
497
|
+
|
|
498
|
+
return resolved_id
|
|
499
|
+
except SlackApiError as e:
|
|
500
|
+
log_and_raise_error(self._logger, f"Could not resolve channel '{channel_clean}': {e.response['error']}")
|
|
501
|
+
|
|
502
|
+
def delete_message(self, channel: str, ts: str) -> dict:
|
|
503
|
+
"""
|
|
504
|
+
Delete a message.
|
|
505
|
+
|
|
506
|
+
Parameters
|
|
507
|
+
----------
|
|
508
|
+
channel : str
|
|
509
|
+
Channel name or ID where the message was posted.
|
|
510
|
+
ts : str
|
|
511
|
+
Timestamp of the message to delete.
|
|
512
|
+
|
|
513
|
+
Returns
|
|
514
|
+
-------
|
|
515
|
+
dict
|
|
516
|
+
API response.
|
|
517
|
+
|
|
518
|
+
Examples
|
|
519
|
+
--------
|
|
520
|
+
>>> slack = SlackConnector(token="xoxb-your-token")
|
|
521
|
+
>>> response = slack.send_message("#general", "Temporary message")
|
|
522
|
+
>>> slack.delete_message("#general", response["ts"])
|
|
523
|
+
"""
|
|
524
|
+
|
|
525
|
+
channel_id = self.get_or_resolve_channel_id(channel)
|
|
526
|
+
|
|
527
|
+
try:
|
|
528
|
+
response = self.client.chat_delete(
|
|
529
|
+
channel=channel_id,
|
|
530
|
+
ts=ts,
|
|
531
|
+
)
|
|
532
|
+
|
|
533
|
+
self._logger.info(f"Message deleted in channel #{channel}, TS {ts}")
|
|
534
|
+
return response.data
|
|
535
|
+
|
|
536
|
+
except SlackApiError as e:
|
|
537
|
+
log_and_raise_error(
|
|
538
|
+
self._logger,
|
|
539
|
+
f"Error deleting message: {e.response['error']}",
|
|
540
|
+
)
|
|
541
|
+
except Exception as e:
|
|
542
|
+
log_and_raise_error(
|
|
543
|
+
self._logger,
|
|
544
|
+
f"Error deleting message: {e}",
|
|
545
|
+
)
|
|
546
|
+
|
|
547
|
+
def add_reaction(self, channel: str, ts: str, emoji: str) -> dict:
|
|
548
|
+
"""
|
|
549
|
+
Add an emoji reaction to a message.
|
|
550
|
+
|
|
551
|
+
Parameters
|
|
552
|
+
----------
|
|
553
|
+
channel : str
|
|
554
|
+
Channel name or ID where the message was posted.
|
|
555
|
+
ts : str
|
|
556
|
+
Timestamp of the message.
|
|
557
|
+
emoji : str
|
|
558
|
+
Emoji name (without colons, e.g., 'thumbsup', 'white_check_mark').
|
|
559
|
+
|
|
560
|
+
Returns
|
|
561
|
+
-------
|
|
562
|
+
dict
|
|
563
|
+
API response.
|
|
564
|
+
|
|
565
|
+
Examples
|
|
566
|
+
--------
|
|
567
|
+
>>> slack = SlackConnector(token="xoxb-your-token")
|
|
568
|
+
>>> response = slack.send_message("#general", "Great work!")
|
|
569
|
+
>>> slack.add_reaction("#general", response["ts"], "thumbsup")
|
|
570
|
+
"""
|
|
571
|
+
|
|
572
|
+
channel_id = self.get_or_resolve_channel_id(channel)
|
|
573
|
+
|
|
574
|
+
emoji = emoji.strip(":")
|
|
575
|
+
|
|
576
|
+
try:
|
|
577
|
+
response = self.client.reactions_add(
|
|
578
|
+
channel=channel_id,
|
|
579
|
+
timestamp=ts,
|
|
580
|
+
name=emoji,
|
|
581
|
+
)
|
|
582
|
+
|
|
583
|
+
self._logger.info(f"Added reaction :{emoji}: to message in #{channel}")
|
|
584
|
+
return response.data
|
|
585
|
+
|
|
586
|
+
except SlackApiError as e:
|
|
587
|
+
log_and_raise_error(
|
|
588
|
+
self._logger,
|
|
589
|
+
f"Error adding reaction: {e.response['error']}",
|
|
590
|
+
)
|
|
591
|
+
except Exception as e:
|
|
592
|
+
log_and_raise_error(
|
|
593
|
+
self._logger,
|
|
594
|
+
f"Error adding reaction: {e}",
|
|
595
|
+
)
|
|
596
|
+
|
|
597
|
+
def delete_file(self, file_id: str) -> dict:
|
|
598
|
+
"""
|
|
599
|
+
Delete a file from Slack.
|
|
600
|
+
|
|
601
|
+
Parameters
|
|
602
|
+
----------
|
|
603
|
+
file_id : str
|
|
604
|
+
The ID of the file to delete (e.g., 'F01234567').
|
|
605
|
+
|
|
606
|
+
Returns
|
|
607
|
+
-------
|
|
608
|
+
dict
|
|
609
|
+
API response.
|
|
610
|
+
|
|
611
|
+
Examples
|
|
612
|
+
--------
|
|
613
|
+
>>> slack = SlackConnector(token="xoxb-your-token")
|
|
614
|
+
>>> # Upload a file and get its ID
|
|
615
|
+
>>> response = slack.send_file("#general", file_path="test.txt")
|
|
616
|
+
>>> file_id = response['file']['id']
|
|
617
|
+
>>> # Later, delete the file
|
|
618
|
+
>>> slack.delete_file(file_id)
|
|
619
|
+
"""
|
|
620
|
+
try:
|
|
621
|
+
response = self.client.files_delete(
|
|
622
|
+
file=file_id,
|
|
623
|
+
)
|
|
624
|
+
|
|
625
|
+
self._logger.info("File deleted successfully")
|
|
626
|
+
return response.data
|
|
627
|
+
|
|
628
|
+
except SlackApiError as e:
|
|
629
|
+
log_and_raise_error(
|
|
630
|
+
self._logger,
|
|
631
|
+
f"Error deleting file: {e.response['error']}",
|
|
632
|
+
)
|
|
633
|
+
except Exception as e:
|
|
634
|
+
log_and_raise_error(
|
|
635
|
+
self._logger,
|
|
636
|
+
f"Error deleting file: {e}",
|
|
637
|
+
)
|