Skip to content

Commit

Permalink
Removes __setattr__ from the Message Interface (ABCMeta) and instead
Browse files Browse the repository at this point in the history
uses property factories defined in _utils.py.  The property factories
are used for credential parameters and any parameter you want to validate
the input with.  __setattr__ called validate_input() for each attribute
assigned.  Now the property factories only validate or obscure certain
attributes.  This commit also edits/adds/dels required unit tests.
Lastly, code formatted with Black.
  • Loading branch information
trp07 committed Nov 10, 2018
1 parent d2c19b4 commit d66bc32
Show file tree
Hide file tree
Showing 20 changed files with 331 additions and 329 deletions.
3 changes: 2 additions & 1 deletion CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ Change Log

Upcoming
--------
-
- Adds code formating with Black
- Removes __setattr__ from Message Interface (ABC) and instead uses a property factory defined in _utils.py


0.4.4
Expand Down
8 changes: 4 additions & 4 deletions messages/_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ def check_config_file(msg):

retrieve_data_from_config(msg, cfg)

if msg.auth is None:
if msg._auth is None:
retrieve_pwd_from_config(msg, cfg)

if msg.save:
Expand Down Expand Up @@ -148,10 +148,10 @@ def update_config_pwd(msg, cfg):
"""
msg_type = msg.__class__.__name__.lower()
key_fmt = msg.profile + "_" + msg_type
if isinstance(msg.auth, (MutableSequence, tuple)):
cfg.pwd[key_fmt] = " :: ".join(msg.auth)
if isinstance(msg._auth, (MutableSequence, tuple)):
cfg.pwd[key_fmt] = " :: ".join(msg._auth)
else:
cfg.pwd[key_fmt] = msg.auth
cfg.pwd[key_fmt] = msg._auth


##############################################################################
Expand Down
9 changes: 5 additions & 4 deletions messages/_exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,11 @@
class InvalidMessageInputError(ValueError):
"""Exception for invalid inputs in message classes."""

def __init__(self, msg_type, attr, input_type):
def __init__(self, msg_type, attr, value, input_type):
self.err = "Invalid input for specified message class: " + msg_type
self.err += '\n\t* argument: "{}"'.format(attr)
self.err += "\n\t* input type must be: {}".format(input_type)
self.err += "\n * param: {}".format(attr)
self.err += "\n * value given: {!r}".format(value)
self.err += "\n * input type must be: {}".format(input_type)
super(InvalidMessageInputError, self).__init__(self.err)


Expand All @@ -17,7 +18,7 @@ class UnsupportedMessageTypeError(TypeError):
def __init__(self, msg_type, msg_types=None):
self.err = "Invalid message type: " + msg_type
if msg_types:
self.err += "\n\t* Supported message types: "
self.err += "\n * Supported message types: "
self.err += str(msg_types)
super(UnsupportedMessageTypeError, self).__init__(self.err)

Expand Down
25 changes: 7 additions & 18 deletions messages/_interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@
from abc import ABCMeta
from abc import abstractmethod

from ._utils import validate_input


class Message(metaclass=ABCMeta):
"""Interface for standard message classes."""
Expand All @@ -14,32 +12,23 @@ class Message(metaclass=ABCMeta):
def send(self):
"""Send message synchronously."""


@abstractmethod
def send_async(self):
"""Send message asynchronously."""


def __setattr__(self, attr, val):
"""Validate attribute inputs after assignment."""
self.__dict__[attr] = val
validate_input(self, attr)


def __repr__(self):
"""repr(self) in debugging format with auth attr obfuscated."""
class_name = type(self).__name__
output = '{}('.format(class_name)
output = "{}(\n".format(class_name)
for attr in self:
if attr == 'auth':
output += 'auth=***obfuscated***,\n'
elif attr == 'body':
output += '{}={!r},\n'.format(attr, reprlib.repr(getattr(self, attr)))
if attr == "_auth":
output += "auth=***obfuscated***,\n"
elif attr == "body":
output += "{}={!r},\n".format(attr, reprlib.repr(getattr(self, attr)))
else:
output += '{}={!r},\n'.format(attr, getattr(self, attr))
output += ')'
output += "{}={!r},\n".format(attr, getattr(self, attr))
output += ")"
return output


def __iter__(self):
return iter(self.__dict__)
99 changes: 67 additions & 32 deletions messages/_utils.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,55 @@
"""Utility Module - functions useful to other modules."""

import datetime
from collections import MutableSequence

import validus

from ._exceptions import InvalidMessageInputError


"""
Functions below this header are used within the _interface.py module in
order to validate user-input to specific fields.
Functions below this header are property factories and input validation functions
used by each of the message classes in order to obscure (credentials) or validate
certain attributes. Each will be defined as a class attribute for each message
(before __init__).
"""


def validate_input(msg, attr, valid=True):
def credential_property(cred):
"""
A credential property factory for each message class that will set
private attributes and return obfuscated credentials when requested.
"""

def getter(instance):
return "***obfuscated***"

def setter(instance, value):
private = "_" + cred
instance.__dict__[private] = value

return property(fget=getter, fset=setter)


def validate_property(attr):
"""
A property factory that will dispatch the to a specific validator function
that will validate the user's input to ensure critical parameters are of a
specific type.
"""

def getter(instance):
return instance.__dict__[attr]

def setter(instance, value):
validate_input(instance.__class__.__name__, attr, value)
instance.__dict__[attr] = value

return property(fget=getter, fset=setter)


def validate_input(msg_type, attr, value):
"""Base function to validate input, dispatched via message type."""
try:
valid = {
Expand All @@ -21,65 +58,63 @@ def validate_input(msg, attr, valid=True):
"SlackWebhook": validate_slackwebhook,
"SlackPost": validate_slackpost,
"TelegramBot": validate_telegrambot,
}[msg.__class__.__name__](msg, attr)
}[msg_type](attr, value)
except KeyError:
pass
return 1
else:
return 0


def check_valid(msg, attr, func, exec_info):
def check_valid(msg_type, attr, value, func, exec_info):
"""
Checker function all validate_* functions below will call.
Raises InvalidMessageInputError if input is not valid as per
given func.
"""
if getattr(msg, attr) is not None:
if isinstance(getattr(msg, attr), list):
for item in getattr(msg, attr):
if not func(item):
raise InvalidMessageInputError(
msg.__class__.__name__, attr, exec_info
)

if value is not None:
if isinstance(value, MutableSequence):
for v in value:
if not func(v):
raise InvalidMessageInputError(msg_type, attr, value, exec_info)
else:
if not func(getattr(msg, attr)):
raise InvalidMessageInputError(msg.__class__.__name__, attr, exec_info)
if not func(value):
raise InvalidMessageInputError(msg_type, attr, value, exec_info)


def validate_email(msg, attr):
def validate_email(attr, value):
"""Email input validator function."""
if attr in ("from_", "to", "cc", "bcc"):
check_valid(msg, attr, validus.isemail, "email address")
check_valid("Email", attr, value, validus.isemail, "email address")


def validate_twilio(msg, attr):
def validate_twilio(attr, value):
"""Twilio input validator function."""
if attr in ("from_", "to"):
check_valid(msg, attr, validus.isphone, "phone number")
check_valid("Twilio", attr, value, validus.isphone, "phone number")
elif attr in ("media_url"):
check_valid(msg, attr, validus.isurl, "url")
check_valid("Twilio", attr, value, validus.isurl, "url")


def validate_slackwebhook(msg, attr):
def validate_slackwebhook(attr, value):
"""SlackWebhook input validator function."""
if attr in ("url", "attachments"):
check_valid(msg, attr, validus.isurl, "url")
check_valid("SlackWebhook", attr, value, validus.isurl, "url")


def validate_slackpost(msg, attr):
def validate_slackpost(attr, value):
"""SlackPost input validator function."""
if attr in ("channel", "credentials"):
if not isinstance(getattr(msg, attr), str):
raise InvalidMessageInputError(msg.__class__.__name__, attr, "string")
if not isinstance(value, str):
raise InvalidMessageInputError("SlackPost", attr, value, "string")
elif attr in ("attachments"):
check_valid("SlackPost", attr, value, validus.isurl, "url")


def validate_telegrambot(msg, attr):
def validate_telegrambot(attr, value):
"""TelegramBot input validator function."""
if attr in ("chat_id"):
check_valid(msg, attr, validus.isint, "integer as a string")
check_valid("TelegramBot", attr, value, validus.isint, "integer as a string")


"""
General utility functions below here.
Functions below this hearder are general utility functions.
"""


Expand Down
17 changes: 16 additions & 1 deletion messages/email_.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
from ._config import check_config_file
from ._eventloop import MESSAGELOOP
from ._interface import Message
from ._utils import credential_property
from ._utils import validate_property
from ._utils import timestamp


Expand Down Expand Up @@ -63,6 +65,13 @@ class Email(Message):
Attributes:
:message: (MIMEMultipart) current form of the message to be constructed
Properties:
:auth: auth will set as a private attribute (_auth) and obscured when requested
:from_: user input will validate a proper email address
:to: user input will be validated for a proper email address
:cc: user input will be validated for a proper email address
:bcc: user input will be validated for a proper email address
Usage:
Create an email object with required Args above.
Send email with self.send() or self.send_async() methods.
Expand All @@ -73,6 +82,12 @@ class Email(Message):
failure may occur when attempting to send.
"""

auth = credential_property("auth")
from_ = validate_property("from_")
to = validate_property("to")
cc = validate_property("cc")
bcc = validate_property("bcc")

def __init__(
self,
from_=None,
Expand Down Expand Up @@ -202,7 +217,7 @@ def get_session(self):
session = self.get_ssl()
elif self.port in (587, "587"):
session = self.get_tls()
session.login(self.from_, self.auth)
session.login(self.from_, self._auth)
return session

def get_ssl(self):
Expand Down
28 changes: 24 additions & 4 deletions messages/slack.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@
from ._config import check_config_file
from ._eventloop import MESSAGELOOP
from ._interface import Message
from ._utils import credential_property
from ._utils import validate_property
from ._utils import timestamp


Expand Down Expand Up @@ -65,16 +67,18 @@ def send(self, encoding="json"):
)

if encoding == "json":
requests.post(self.url, json=self.message)
resp = requests.post(self.url, json=self.message)
elif encoding == "url":
requests.post(self.url, data=self.message)
resp = requests.post(self.url, data=self.message)

if self.verbose:
print(
timestamp(),
type(self).__name__,
" info:",
self.__str__(indentation="\n * "),
"\nresponse code:",
resp.status_code,
)

print("Message sent.")
Expand Down Expand Up @@ -105,6 +109,10 @@ class SlackWebhook(Slack):
Attributes:
:message: (dict) current form of the message to be constructed
Properties:
:auth: auth will set as a private attribute (_auth) and obscured when requested
:attachments: user input will be validated for a proper url
Usage:
Create a SlackWebhook object with required Args above.
Send message with self.send() or self.send_async() methods.
Expand All @@ -114,6 +122,9 @@ class SlackWebhook(Slack):
https://api.slack.com/incoming-webhooks
"""

auth = credential_property("auth")
attachments = validate_property("attachments")

def __init__(
self,
from_=None,
Expand Down Expand Up @@ -141,7 +152,7 @@ def __init__(
if self.profile:
check_config_file(self)

self.url = self.auth
self.url = self._auth

def __str__(self, indentation="\n"):
"""print(SlackWebhook(**args)) method.
Expand Down Expand Up @@ -189,6 +200,11 @@ class SlackPost(Slack):
Attributes:
:message: (dict) current form of the message to be constructed
Properties:
:auth: auth will set as a private attribute (_auth) and obscured when requested
:attachments: user input will be validated for a proper url
:channel: user input will be validated for a proper string
Usage:
Create a SlackPost object with required Args above.
Send message with self.send() or self.send_async() methods.
Expand All @@ -198,6 +214,10 @@ class SlackPost(Slack):
https://api.slack.com/methods/chat.postMessage
"""

auth = credential_property("auth")
attachments = validate_property("attachments")
channel = validate_property("channel")

def __init__(
self,
from_=None,
Expand Down Expand Up @@ -227,7 +247,7 @@ def __init__(
if self.profile:
check_config_file(self)

self.message = {"token": self.auth, "channel": self.channel}
self.message = {"token": self._auth, "channel": self.channel}

def __str__(self, indentation="\n"):
"""print(SlackPost(**args)) method.
Expand Down
Loading

0 comments on commit d66bc32

Please sign in to comment.