Source code for phial.wrappers

"""Contains models for phial to use."""

import re
from typing import IO, Callable, Dict, List, Optional, Pattern, Union

#: A union of all response types phial can use
PhialResponse = Union[None, str, "Response", "Attachment"]


[docs]class Response: r""" A response to be sent to Slack. When returned in a command function will send a message, or reaction to Slack depending on contents. :param channel: The Slack channel ID the response will be sent to :param text: The response contents :param original_ts: The timestamp of the original message. If populated will put the text response in a thread :param reation: A valid slack emoji name. NOTE: will only work when :code:`original_ts` is populated :param attachments: Any Slack `Message Attachments <https://api.slack.com/docs/\ message-attachments#attachment_structure>`_ :param ephemeral: Whether to send the message as an ephemeral message :param user: The user id to display the ephemeral message to .. rubric:: Examples The following would send a message to a slack channel when executed :: @slackbot.command('ping') def ping(): return Response(text="Pong", channel='channel_id') The following would send a reply to a message in a thread :: @slackbot.command('hello') def hello(): return Response(text="hi", channel='channel_id', original_ts='original_ts') The following would send a reaction to a message :: @slackbot.command('react') def react(): return Response(reaction="x", channel='channel_id', original_ts='original_ts') """ def __init__( self, channel: str, text: Optional[str] = None, original_ts: Optional[str] = None, reaction: Optional[str] = None, ephemeral: bool = False, user: Optional[str] = None, attachments: Optional[ List[Dict[str, Union[str, int, float, bool, List]]] ] = None, ) -> None: self.channel = channel self.text = text self.original_ts = original_ts self.reaction = reaction self.ephemeral = ephemeral self.user = user self.attachments = attachments def __repr__(self) -> str: return "<Response: {0}>".format(self.text) def __eq__(self, other: object) -> bool: return self.__dict__ == other.__dict__
[docs]class Attachment: """ A file to be uploaded to Slack. :param channel: The Slack channel ID the file will be sent to :param filename: The filename of the file :param content: The file to send to Slack. Open file using open('<file>', 'rb') .. rubric:: Example :: Attachment('channel', 'file_name', open('file', 'rb')) """ def __init__(self, channel: str, filename: str, content: IO) -> None: self.channel = channel self.filename = filename self.content = content def __repr__(self) -> str: return "<Attachment {0} in {1}>".format(self.filename, self.channel)
[docs]class Message: """ A representation of a Slack message. :param text: The message contents :param channel: The Slack channel ID the message was sent from :param user: The user who sent the message :param timestamp: The message's timestamp :param team: The Team ID of the Slack workspace the message was sent from :param bot_id: If the message was sent by a bot the ID of that bot. Defaults to None. """ def __init__( self, text: str, channel: str, user: str, timestamp: str, team: Optional[str], bot_id: Optional[str] = None, ) -> None: self.text = text self.channel = channel self.user = user self.timestamp = timestamp self.team = team self.bot_id = bot_id def __repr__(self) -> str: return "<Message: {0} in {1}:{2} at {3}>".format( self.text, self.channel, self.team, self.timestamp ) def __eq__(self, other: object) -> bool: return self.__dict__ == other.__dict__
[docs]class Command: """ An action executable from Slack. :param pattern: A string that a Slack Message must match to trigger execution :param func: The function that will be triggered when the command is invoked :param case_sensitive: Whether the :code:`pattern` should enforce case sensitivity :param help_text_override: Overrides the function's docstring in the standard help command :param hide_from_help_command: Prevents function from being displayed by the standard help command """ def __init__( self, pattern: str, func: Callable[..., PhialResponse], case_sensitive: bool = False, help_text_override: Optional[str] = None, hide_from_help_command: Optional[bool] = False, ): self.pattern_string = pattern self.pattern = self._build_pattern_regex(pattern, case_sensitive) self.alias_patterns = self._get_alias_patterns(func) self.func = func self.case_sensitive = case_sensitive self.help_text_override = help_text_override self.hide_from_help_command = hide_from_help_command def __repr__(self) -> str: return "<Command: {0}>".format(self.pattern_string) def _get_alias_patterns(self, func: Callable) -> List[Pattern]: patterns: List[Pattern] = [] if hasattr(func, "alias_patterns"): for pattern in func.alias_patterns: # type: ignore patterns.append(self._build_pattern_regex(pattern)) return patterns @staticmethod def _build_pattern_regex( pattern: str, case_sensitive: bool = False ) -> Pattern[str]: command = re.sub(r"(<\w+>)", r"(\"?\1\"?)", pattern) command_regex = re.sub(r"(<\w+>)", r'(?P\1[^"]*)', command) return re.compile( "^{}$".format(command_regex), 0 if case_sensitive else re.IGNORECASE )
[docs] def pattern_matches(self, message: Message) -> Optional[Dict[str, str]]: """ Check if message should invoke the command. Checks whether the text of a :obj:`Message` matches the command's pattern, or any of it's aliased patterns. """ match = self.pattern.match(message.text) if match: return match.groupdict() # Only check aliases if main pattern does not match for alias in self.alias_patterns: match = alias.match(message.text) if match: return match.groupdict() return None
@property def help_text(self) -> Optional[str]: """A description of the command's function.""" if self.help_text_override is not None: return self.help_text_override return self.func.__doc__