Source code for dmf.alerts.slack_backend

import logging

from datetime import datetime, timedelta
from pathlib import Path
from typing import Optional, Union

try:
    from slack_sdk import WebClient
    from slack_sdk.errors import SlackApiError
except ImportError:
    raise ImportError(
        "The 'slack_sdk' package is required to use the Slack backend."
        "You can install it with 'pip install dmf-utils[alerts]'."
    )

from .backend import AlertBackend, AlertException
from ..env import env

__all__ = ["SlackBackend"]

DEFAULT_CHANNEL_ENV = "DMF_DEFAULT_CHANNEL"


[docs] class SlackBackend(AlertBackend): """ A backend for sending alerts through Slack. This class allows you to send alerts to a specified Slack channel using a bot token. It supports sending plain text messages, as well as messages with attachments. To use this backend, you'll need to provide a Slack bot token and specify a channel where the alerts should be sent. **Getting a Slack Token**: 1. Go to the Slack API page and create a new Slack App: https://api.slack.com/apps 2. Under "OAuth & Permissions", generate a "Bot User OAuth Token". 3. This token is required to authenticate your bot and should be passed as the `token` parameter. 4. Ensure the following OAuth scopes are granted to your app: - `chat:write`: Allows the bot to send messages to channels. - `files:write`: Allows the bot to upload and share files in channels. - `chat:write.public`: Allows the bot to post in public channels without being specifically invited to them. **Channel Parameter**: - The `channel` parameter can be either the name (e.g., "#general") or the ID (e.g., "C01234567") of a Slack channel. - The channel can be public or private. - If you're sending messages to a private channel, ensure that your Slack bot has been invited to that channel. **Example Usage**: To send a simple message using the Slack backend:: backend = SlackBackend(token="xoxb-your-slack-bot-token", channel="#general") backend("Hello, world!") :param token: The Slack bot token used for authentication. This token is necessary to send messages to Slack. :param channel: Optional; The Slack channel where messages should be sent. Can be specified by name or ID. If not provided, the channel will be determined by the `DMF_DEFAULT_CHANNEL` environment variable. :param fail_silently: Optional; If True, errors will be logged instead of raising an exception. Default is True. """
[docs] def __init__( self, token: str, channel: Optional[str] = None, fail_silently: bool = True ): """ Initialize the Slack backend with the token and default channel. :param token: The Slack bot token used for authentication. This token is necessary to send messages to Slack. :param channel: Optional; The Slack channel where messages should be sent. Can be specified by name or ID. If not provided, the channel will be determined by the `DMF_DEFAULT_CHANNEL` environment variable. :param fail_silently: Optional; If True, errors will be logged instead of raising an exception. Default is True. """ super().__init__(fail_silently=fail_silently) self.client = WebClient(token=token) self.channel = channel or env.getenv(DEFAULT_CHANNEL_ENV)
[docs] def send_message( self, text: str = "", attachment: Optional[Union[str, Path]] = None, scheduled_time: Optional[Union[int, datetime]] = None, ) -> None: """ Send a message to a Slack channel, optionally with a file attachment or scheduled time. Raises AlertException if an error occurs, or if both attachment and scheduled_time are provided. """ if attachment and scheduled_time: if not self.fail_silently: raise AlertException( "Cannot send a message with both an attachment and a scheduled time." ) else: logging.warning( "Cannot send a message with both an attachment and a scheduled time. " "Ignoring scheduled time." ) scheduled_time = None try: if scheduled_time: self._send_scheduled_message(text, self.channel, scheduled_time) elif attachment: self._send_attachment_message(text, self.channel, attachment) else: self._send_message(text, self.channel) except SlackApiError as error: if self.fail_silently: logging.error(f"Error sending message: {error.response['error']}") else: raise AlertException( f"Error sending message: {error.response['error']}" ) from error
def __repr__(self) -> str: return f"<{self.__class__.__name__} channel={self.channel}>" def _send_scheduled_message( self, text: str, channel: str, post_at: Union[int, "datetime", "timedelta"] ) -> dict: """Send a scheduled message to Slack.""" # Convert timedelta to Unix timestamp if needed if isinstance(post_at, timedelta): post_at = int((datetime.now() + post_at).timestamp()) # Convert datetime to Unix timestamp if needed if isinstance(post_at, datetime): post_at = int(post_at.timestamp()) return self.client.chat_scheduleMessage( channel=channel, text=text, post_at=post_at ) def _send_attachment_message( self, text: str, channel: str, attachment: Union[str, Path] ) -> dict: """Send a message with an attachment to Slack.""" file_path = str(attachment) # Ensure the path is a string return self.client.files_upload_v2( channel=channel, file=file_path, initial_comment=text ) def _send_message(self, text: str, channel: str) -> dict: """Send a simple message to Slack.""" return self.client.chat_postMessage(channel=channel, text=text)