import logging
from http import HTTPStatus
from enum import Enum
from flask import current_app as app
from flask import jsonify, make_response, Response
from typing import Dict, List
from amundsen_application.api.exceptions import MailClientNotImplemented
from amundsen_application.log.action_log import action_logging
class NotificationType(str, Enum):
"""
Enum to describe supported notification types. Must match NotificationType interface defined in:
https://github.com/lyft/amundsenfrontendlibrary/blob/master/amundsen_application/static/js/interfaces/Notifications.ts
"""
OWNER_ADDED = 'owner_added'
OWNER_REMOVED = 'owner_removed'
METADATA_EDITED = 'metadata_edited'
METADATA_REQUESTED = 'metadata_requested'
@classmethod
def has_value(cls, value: str) -> bool:
for key in cls:
if key.value == value:
return True
return False
NOTIFICATION_STRINGS = {
NotificationType.OWNER_ADDED.value: {
'comment': ('
What is expected of you?
As an owner, you take an important part in making '
'sure that the datasets you own can be used as swiftly as possible across the company.
'
'Make sure the metadata is correct and up to date.
'),
'end_note': ('
If you think you are not the best person to own this dataset and know someone who might '
'be, please contact this person and ask them if they want to replace you. It is important that we '
'keep multiple owners for each dataset to ensure continuity.
'),
'notification': ('
You have been added to the owners list of the '
'{resource_name} dataset by {sender}.
'),
},
NotificationType.OWNER_REMOVED.value: {
'comment': '',
'end_note': ('
If you think you have been incorrectly removed as an owner, '
'add yourself back to the owners list.
'),
'notification': ('
You have been removed from the owners list of the '
'{resource_name} dataset by {sender}.
'),
},
NotificationType.METADATA_REQUESTED.value: {
'comment': '',
'end_note': '
Please visit the provided link and improve descriptions on that resource.
',
'notification': '
{sender} is trying to use {resource_name}, ',
}
}
def get_mail_client(): # type: ignore
"""
Gets a mail_client object to send emails, raises an exception
if mail client isn't implemented
"""
mail_client = app.config['MAIL_CLIENT']
if not mail_client:
raise MailClientNotImplemented('An instance of BaseMailClient client must be configured on MAIL_CLIENT')
return mail_client
def validate_options(*, options: Dict) -> None:
"""
Raises an Exception if the options do not contain resource_path or resource_name
"""
if options.get('resource_path') is None:
raise Exception('resource_path was not provided in the notification options')
if options.get('resource_name')is None:
raise Exception('resource_name was not provided in the notification options')
def get_notification_html(*, notification_type: str, options: Dict, sender: str) -> str:
"""
Returns the formatted html for the notification based on the notification_type
:return: A string representing the html markup to send in the notification
"""
validate_options(options=options)
url_base = app.config['FRONTEND_BASE']
resource_url = '{url_base}{resource_path}?source=notification'.format(resource_path=options.get('resource_path'),
url_base=url_base)
joined_chars = resource_url[len(url_base) - 1:len(url_base) + 1]
if joined_chars.count('/') != 1:
raise Exception('Configured "FRONTEND_BASE" and "resource_path" do not form a valid url')
notification_strings = NOTIFICATION_STRINGS.get(notification_type)
if notification_strings is None:
raise Exception('Unsupported notification_type')
greeting = 'Hello,
'
notification = notification_strings.get('notification', '').format(resource_url=resource_url,
resource_name=options.get('resource_name'),
sender=sender)
comment = notification_strings.get('comment', '')
end_note = notification_strings.get('end_note', '')
salutation = '
Thanks,
Amundsen Team'
if notification_type == NotificationType.METADATA_REQUESTED:
options_comment = options.get('comment')
need_resource_description = options.get('description_requested')
need_fields_descriptions = options.get('fields_requested')
if need_resource_description and need_fields_descriptions:
notification = notification + 'and requests improved table and column descriptions.
'
elif need_resource_description:
notification = notification + 'and requests an improved table description.
'
elif need_fields_descriptions:
notification = notification + 'and requests improved column descriptions.
'
else:
notification = notification + 'and requests more information about that resource.
'
if options_comment:
comment = ('
{sender} has included the following information with their request:'
'
{comment}
').format(sender=sender, comment=options_comment)
return '{greeting}{notification}{comment}{end_note}{salutation}'.format(greeting=greeting,
notification=notification,
comment=comment,
end_note=end_note,
salutation=salutation)
def get_notification_subject(*, notification_type: str, options: Dict) -> str:
"""
Returns the subject to use for the given notification_type
:param notification_type: type of notification
:param options: data necessary to render email template content
:return: The subject to be used with the notification
"""
resource_name = options.get('resource_name')
notification_subject_dict = {
NotificationType.OWNER_ADDED.value: 'You are now an owner of {}'.format(resource_name),
NotificationType.OWNER_REMOVED.value: 'You have been removed as an owner of {}'.format(resource_name),
NotificationType.METADATA_EDITED.value: 'Your dataset {}\'s metadata has been edited'.format(resource_name),
NotificationType.METADATA_REQUESTED.value: 'Request for metadata on {}'.format(resource_name),
}
subject = notification_subject_dict.get(notification_type)
if subject is None:
raise Exception('Unsupported notification_type')
return subject
def send_notification(*, notification_type: str, options: Dict, recipients: List, sender: str) -> Response:
"""
Sends a notification via email to a given list of recipients
:param notification_type: type of notification
:param options: data necessary to render email template content
:param recipients: list of recipients who should receive notification
:param sender: email of notification sender
:return: Response
"""
@action_logging
def _log_send_notification(*, notification_type: str, options: Dict, recipients: List, sender: str) -> None:
""" Logs the content of a sent notification"""
pass # pragma: no cover
try:
if not app.config['NOTIFICATIONS_ENABLED']:
message = 'Notifications are not enabled. Request was accepted but no notification will be sent.'
logging.exception(message)
return make_response(jsonify({'msg': message}), HTTPStatus.ACCEPTED)
if sender in recipients:
recipients.remove(sender)
if len(recipients) == 0:
logging.info('No recipients exist for notification')
return make_response(
jsonify({
'msg': 'No valid recipients exist for notification, notification was not sent.'
}),
HTTPStatus.OK
)
mail_client = get_mail_client()
html = get_notification_html(notification_type=notification_type, options=options, sender=sender)
subject = get_notification_subject(notification_type=notification_type, options=options)
_log_send_notification(
notification_type=notification_type,
options=options,
recipients=recipients,
sender=sender
)
response = mail_client.send_email(
recipients=recipients,
sender=sender,
subject=subject,
html=html,
optional_data={
'email_type': notification_type,
},
)
status_code = response.status_code
if status_code == HTTPStatus.OK:
message = 'Success'
else:
message = 'Mail client failed with status code ' + str(status_code)
logging.error(message)
return make_response(jsonify({'msg': message}), status_code)
except MailClientNotImplemented as e:
message = 'Encountered exception: ' + str(e)
logging.exception(message)
return make_response(jsonify({'msg': message}), HTTPStatus.NOT_IMPLEMENTED)
except Exception as e1:
message = 'Encountered exception: ' + str(e1)
logging.exception(message)
return make_response(jsonify({'msg': message}), HTTPStatus.INTERNAL_SERVER_ERROR)