notify-api-412 use black to enforce python style standards

This commit is contained in:
Kenneth Kehl
2023-08-23 10:35:43 -07:00
parent a7898118d7
commit 026dc14021
586 changed files with 33990 additions and 23461 deletions

View File

@@ -5,26 +5,28 @@ AWS_CLIENT_CONFIG = Config(
# endpoints. See https://aws.amazon.com/compliance/fips/ for more
# information.
s3={
'addressing_style': 'virtual',
"addressing_style": "virtual",
},
use_fips_endpoint=True
use_fips_endpoint=True,
)
STATISTICS_REQUESTED = 'requested'
STATISTICS_DELIVERED = 'delivered'
STATISTICS_FAILURE = 'failure'
STATISTICS_REQUESTED = "requested"
STATISTICS_DELIVERED = "delivered"
STATISTICS_FAILURE = "failure"
class ClientException(Exception):
'''
"""
Base Exceptions for sending notifications that fail
'''
"""
pass
class Client(object):
'''
"""
Base client for sending notifications.
'''
"""
pass
@@ -46,10 +48,10 @@ class NotificationProviderClients(object):
return self.email_clients.get(name)
def get_client_by_name_and_type(self, name, notification_type):
assert notification_type in ['email', 'sms'] # nosec B101
assert notification_type in ["email", "sms"] # nosec B101
if notification_type == 'email':
if notification_type == "email":
return self.get_email_client(name)
if notification_type == 'sms':
if notification_type == "sms":
return self.get_sms_client(name)

View File

@@ -19,7 +19,7 @@ class AwsCloudwatchClient(Client):
region_name=cloud_config.sns_region,
aws_access_key_id=cloud_config.sns_access_key,
aws_secret_access_key=cloud_config.sns_secret_key,
config=AWS_CLIENT_CONFIG
config=AWS_CLIENT_CONFIG,
)
super(Client, self).__init__(*args, **kwargs)
self.current_app = current_app
@@ -27,10 +27,9 @@ class AwsCloudwatchClient(Client):
@property
def name(self):
return 'cloudwatch'
return "cloudwatch"
def _get_log(self, my_filter, log_group_name, sent_at):
# Check all cloudwatch logs from the time the notification was sent (currently 5 minutes previously) until now
now = round(time.time() * 1000)
beginning = sent_at
@@ -43,48 +42,51 @@ class AwsCloudwatchClient(Client):
filterPattern=my_filter,
nextToken=next_token,
startTime=beginning,
endTime=now
endTime=now,
)
else:
response = self._client.filter_log_events(
logGroupName=log_group_name,
filterPattern=my_filter,
startTime=beginning,
endTime=now
endTime=now,
)
log_events = response.get('events', [])
log_events = response.get("events", [])
all_log_events.extend(log_events)
if len(log_events) > 0:
# We found it
break
next_token = response.get('nextToken')
next_token = response.get("nextToken")
if not next_token:
break
return all_log_events
def check_sms(self, message_id, notification_id, created_at):
# TODO this clumsy approach to getting the account number will be fixed as part of notify-api #258
account_number = cloud_config.ses_domain_arn
account_number = account_number.replace('arn:aws:ses:us-west-2:', '')
account_number = account_number.replace("arn:aws:ses:us-west-2:", "")
account_number = account_number.split(":")
account_number = account_number[0]
log_group_name = f'sns/us-west-2/{account_number}/DirectPublishToPhoneNumber'
log_group_name = f"sns/us-west-2/{account_number}/DirectPublishToPhoneNumber"
filter_pattern = '{$.notification.messageId="XXXXX"}'
filter_pattern = filter_pattern.replace("XXXXX", message_id)
all_log_events = self._get_log(filter_pattern, log_group_name, created_at)
if all_log_events and len(all_log_events) > 0:
event = all_log_events[0]
message = json.loads(event['message'])
return "success", message['delivery']['providerResponse']
message = json.loads(event["message"])
return "success", message["delivery"]["providerResponse"]
log_group_name = f'sns/us-west-2/{account_number}/DirectPublishToPhoneNumber/Failure'
log_group_name = (
f"sns/us-west-2/{account_number}/DirectPublishToPhoneNumber/Failure"
)
all_failed_events = self._get_log(filter_pattern, log_group_name, created_at)
if all_failed_events and len(all_failed_events) > 0:
event = all_failed_events[0]
message = json.loads(event['message'])
return "failure", message['delivery']['providerResponse']
message = json.loads(event["message"])
return "failure", message["delivery"]["providerResponse"]
raise Exception(f'No event found for message_id {message_id} notification_id {notification_id}')
raise Exception(
f"No event found for message_id {message_id} notification_id {notification_id}"
)

View File

@@ -9,16 +9,15 @@ class DocumentDownloadError(Exception):
@classmethod
def from_exception(cls, e):
message = e.response.json()['error']
message = e.response.json()["error"]
status_code = e.response.status_code
return cls(message, status_code)
class DocumentDownloadClient:
def init_app(self, app):
self.api_host = app.config['DOCUMENT_DOWNLOAD_API_HOST']
self.auth_token = app.config['DOCUMENT_DOWNLOAD_API_KEY']
self.api_host = app.config["DOCUMENT_DOWNLOAD_API_HOST"]
self.auth_token = app.config["DOCUMENT_DOWNLOAD_API_KEY"]
def get_upload_url(self, service_id):
return "{}/services/{}/documents".format(self.api_host, service_id)
@@ -28,12 +27,12 @@ class DocumentDownloadClient:
response = requests.post(
self.get_upload_url(service_id),
headers={
'Authorization': "Bearer {}".format(self.auth_token),
"Authorization": "Bearer {}".format(self.auth_token),
},
json={
'document': file_contents,
'is_csv': is_csv or False,
}
"document": file_contents,
"is_csv": is_csv or False,
},
)
response.raise_for_status()
@@ -42,14 +41,16 @@ class DocumentDownloadClient:
# we don't want to tell users about that, so anything that isn't a 400 (virus scan failed or file type
# unrecognised) should be raised as a 500 internal server error here.
if e.response is None:
raise Exception(f'Unhandled document download error: {repr(e)}')
raise Exception(f"Unhandled document download error: {repr(e)}")
elif e.response.status_code == 400:
error = DocumentDownloadError.from_exception(e)
current_app.logger.info(
'Document download request failed with error: {}'.format(error.message)
"Document download request failed with error: {}".format(
error.message
)
)
raise error
else:
raise Exception(f'Unhandled document download error: {e.response.text}')
raise Exception(f"Unhandled document download error: {e.response.text}")
return response.json()['document']['url']
return response.json()["document"]["url"]

View File

@@ -2,32 +2,34 @@ from app.clients import Client, ClientException
class EmailClientException(ClientException):
'''
"""
Base Exception for EmailClients
'''
"""
pass
class EmailClientNonRetryableException(ClientException):
'''
"""
Represents an error returned from the email client API with a 4xx response code
that should not be retried and should instead be marked as technical failure.
An example of this would be an email address that makes it through our
validation rules but is rejected by SES. There is no point in retrying this type as
it will always fail however many calls to SES. Whereas a throttling error would not
use this exception as it may succeed if we retry
'''
"""
pass
class EmailClient(Client):
'''
"""
Base Email client for sending emails.
'''
"""
def send_email(self, *args, **kwargs):
raise NotImplementedError('TODO Need to implement.')
raise NotImplementedError("TODO Need to implement.")
@property
def name(self):
raise NotImplementedError('TODO Need to implement.')
raise NotImplementedError("TODO Need to implement.")

View File

@@ -17,30 +17,30 @@ from app.clients.email import (
from app.cloudfoundry_config import cloud_config
ses_response_map = {
'Permanent': {
"message": 'Hard bounced',
"Permanent": {
"message": "Hard bounced",
"success": False,
"notification_status": 'permanent-failure',
"notification_statistics_status": STATISTICS_FAILURE
"notification_status": "permanent-failure",
"notification_statistics_status": STATISTICS_FAILURE,
},
'Temporary': {
"message": 'Soft bounced',
"Temporary": {
"message": "Soft bounced",
"success": False,
"notification_status": 'temporary-failure',
"notification_statistics_status": STATISTICS_FAILURE
"notification_status": "temporary-failure",
"notification_statistics_status": STATISTICS_FAILURE,
},
'Delivery': {
"message": 'Delivered',
"Delivery": {
"message": "Delivered",
"success": True,
"notification_status": 'delivered',
"notification_statistics_status": STATISTICS_DELIVERED
"notification_status": "delivered",
"notification_statistics_status": STATISTICS_DELIVERED,
},
'Complaint': {
"message": 'Complaint',
"Complaint": {
"message": "Complaint",
"success": True,
"notification_status": 'delivered',
"notification_statistics_status": STATISTICS_DELIVERED
}
"notification_status": "delivered",
"notification_statistics_status": STATISTICS_DELIVERED,
},
}
@@ -57,61 +57,57 @@ class AwsSesClientThrottlingSendRateException(AwsSesClientException):
class AwsSesClient(EmailClient):
'''
"""
Amazon SES email client.
'''
"""
def init_app(self, *args, **kwargs):
self._client = client(
'ses',
"ses",
region_name=cloud_config.ses_region,
aws_access_key_id=cloud_config.ses_access_key,
aws_secret_access_key=cloud_config.ses_secret_key,
config=AWS_CLIENT_CONFIG
config=AWS_CLIENT_CONFIG,
)
super(AwsSesClient, self).__init__(*args, **kwargs)
@property
def name(self):
return 'ses'
return "ses"
def send_email(self,
source,
to_addresses,
subject,
body,
html_body='',
reply_to_address=None):
def send_email(
self, source, to_addresses, subject, body, html_body="", reply_to_address=None
):
try:
if isinstance(to_addresses, str):
to_addresses = [to_addresses]
reply_to_addresses = [reply_to_address] if reply_to_address else []
body = {
'Text': {'Data': body}
}
body = {"Text": {"Data": body}}
if html_body:
body.update({
'Html': {'Data': html_body}
})
body.update({"Html": {"Data": html_body}})
start_time = monotonic()
response = self._client.send_email(
Source=source,
Destination={
'ToAddresses': [punycode_encode_email(addr) for addr in to_addresses],
'CcAddresses': [],
'BccAddresses': []
"ToAddresses": [
punycode_encode_email(addr) for addr in to_addresses
],
"CcAddresses": [],
"BccAddresses": [],
},
Message={
'Subject': {
'Data': subject,
"Subject": {
"Data": subject,
},
'Body': body
"Body": body,
},
ReplyToAddresses=[punycode_encode_email(addr) for addr in reply_to_addresses]
ReplyToAddresses=[
punycode_encode_email(addr) for addr in reply_to_addresses
],
)
except botocore.exceptions.ClientError as e:
_do_fancy_exception_handling(e)
@@ -120,23 +116,25 @@ class AwsSesClient(EmailClient):
raise AwsSesClientException(str(e))
else:
elapsed_time = monotonic() - start_time
current_app.logger.info("AWS SES request finished in {}".format(elapsed_time))
return response['MessageId']
current_app.logger.info(
"AWS SES request finished in {}".format(elapsed_time)
)
return response["MessageId"]
def punycode_encode_email(email_address):
# only the hostname should ever be punycode encoded.
local, hostname = email_address.split('@')
return '{}@{}'.format(local, hostname.encode('idna').decode('utf-8'))
local, hostname = email_address.split("@")
return "{}@{}".format(local, hostname.encode("idna").decode("utf-8"))
def _do_fancy_exception_handling(e):
# http://docs.aws.amazon.com/ses/latest/DeveloperGuide/api-error-codes.html
if e.response['Error']['Code'] == 'InvalidParameterValue':
raise EmailClientNonRetryableException(e.response['Error']['Message'])
if e.response["Error"]["Code"] == "InvalidParameterValue":
raise EmailClientNonRetryableException(e.response["Error"]["Message"])
elif (
e.response['Error']['Code'] == 'Throttling'
and e.response['Error']['Message'] == 'Maximum sending rate exceeded.'
e.response["Error"]["Code"] == "Throttling"
and e.response["Error"]["Message"] == "Maximum sending rate exceeded."
):
raise AwsSesClientThrottlingSendRateException(str(e))
else:

View File

@@ -17,23 +17,14 @@ class AwsSesStubClient(EmailClient):
@property
def name(self):
return 'ses'
return "ses"
def send_email(self,
source,
to_addresses,
subject,
body,
html_body='',
reply_to_address=None):
def send_email(
self, source, to_addresses, subject, body, html_body="", reply_to_address=None
):
try:
start_time = monotonic()
response = request(
"POST",
self.url,
data={"id": "dummy-data"},
timeout=60
)
response = request("POST", self.url, data={"id": "dummy-data"}, timeout=60)
response.raise_for_status()
response_json = json.loads(response.text)
@@ -41,5 +32,7 @@ class AwsSesStubClient(EmailClient):
raise AwsSesStubClientException(str(e))
else:
elapsed_time = monotonic() - start_time
current_app.logger.info("AWS SES stub request finished in {}".format(elapsed_time))
return response_json['MessageId']
current_app.logger.info(
"AWS SES stub request finished in {}".format(elapsed_time)
)
return response_json["MessageId"]

View File

@@ -6,45 +6,49 @@ from flask import current_app
class PerformancePlatformClient:
@property
def active(self):
return self._active
def init_app(self, app):
self._active = app.config.get('PERFORMANCE_PLATFORM_ENABLED')
self._active = app.config.get("PERFORMANCE_PLATFORM_ENABLED")
if self.active:
self.performance_platform_url = app.config.get('PERFORMANCE_PLATFORM_URL')
self.performance_platform_endpoints = app.config.get('PERFORMANCE_PLATFORM_ENDPOINTS')
self.performance_platform_url = app.config.get("PERFORMANCE_PLATFORM_URL")
self.performance_platform_endpoints = app.config.get(
"PERFORMANCE_PLATFORM_ENDPOINTS"
)
def send_stats_to_performance_platform(self, payload):
if self.active:
bearer_token = self.performance_platform_endpoints[payload['dataType']]
bearer_token = self.performance_platform_endpoints[payload["dataType"]]
headers = {
'Content-Type': "application/json",
'Authorization': 'Bearer {}'.format(bearer_token)
"Content-Type": "application/json",
"Authorization": "Bearer {}".format(bearer_token),
}
resp = requests.post(
self.performance_platform_url + payload['dataType'],
self.performance_platform_url + payload["dataType"],
json=payload,
headers=headers
headers=headers,
)
if resp.status_code == 200:
current_app.logger.info(
"Updated performance platform successfully with payload {}".format(json.dumps(payload))
"Updated performance platform successfully with payload {}".format(
json.dumps(payload)
)
)
else:
current_app.logger.error(
"Performance platform update request failed for payload with response details: {} '{}'".format(
json.dumps(payload),
resp.status_code
json.dumps(payload), resp.status_code
)
)
resp.raise_for_status()
@staticmethod
def format_payload(*, dataset, start_time, group_name, group_value, count, period='day'):
def format_payload(
*, dataset, start_time, group_name, group_value, count, period="day"
):
"""
:param dataset - the name of the overall graph, as referred to in the endpoint.
:param start_time - UTC midnight of the day we're sending stats for
@@ -54,14 +58,16 @@ class PerformancePlatformClient:
:param period - the period that this data covers - "day", "week", "month", "quarter".
"""
payload = {
'_timestamp': start_time,
'service': 'govuk-notify',
'dataType': dataset,
'period': period,
'count': count,
"_timestamp": start_time,
"service": "govuk-notify",
"dataType": dataset,
"period": period,
"count": count,
group_name: group_value,
}
payload['_id'] = PerformancePlatformClient.generate_payload_id(payload, group_name)
payload["_id"] = PerformancePlatformClient.generate_payload_id(
payload, group_name
)
return payload
@staticmethod
@@ -69,12 +75,12 @@ class PerformancePlatformClient:
"""
group_name is the name of the group - eg "channel" or "status"
"""
payload_string = '{}{}{}{}{}'.format(
payload['_timestamp'],
payload['service'],
payload_string = "{}{}{}{}{}".format(
payload["_timestamp"],
payload["service"],
payload[group_name],
payload['dataType'],
payload['period']
payload["dataType"],
payload["period"],
)
_id = base64.b64encode(payload_string.encode('utf-8'))
return _id.decode('utf-8')
_id = base64.b64encode(payload_string.encode("utf-8"))
return _id.decode("utf-8")

View File

@@ -21,7 +21,7 @@ class AwsSnsClient(SmsClient):
region_name=cloud_config.sns_region,
aws_access_key_id=cloud_config.sns_access_key,
aws_secret_access_key=cloud_config.sns_secret_key,
config=AWS_CLIENT_CONFIG
config=AWS_CLIENT_CONFIG,
)
super(SmsClient, self).__init__(*args, **kwargs)
self.current_app = current_app
@@ -29,7 +29,7 @@ class AwsSnsClient(SmsClient):
@property
def name(self):
return 'sns'
return "sns"
def get_name(self):
return self.name
@@ -42,7 +42,9 @@ class AwsSnsClient(SmsClient):
for match in phonenumbers.PhoneNumberMatcher(to, "US"):
matched = True
to = phonenumbers.format_number(match.number, phonenumbers.PhoneNumberFormat.E164)
to = phonenumbers.format_number(
match.number, phonenumbers.PhoneNumberFormat.E164
)
# See documentation
# https://docs.aws.amazon.com/sns/latest/dg/sms_publish-to-phone.html#sms_publish_sdk
@@ -66,14 +68,18 @@ class AwsSnsClient(SmsClient):
try:
start_time = monotonic()
response = self._client.publish(PhoneNumber=to, Message=content, MessageAttributes=attributes)
response = self._client.publish(
PhoneNumber=to, Message=content, MessageAttributes=attributes
)
except botocore.exceptions.ClientError as e:
raise str(e)
except Exception as e:
raise str(e)
finally:
elapsed_time = monotonic() - start_time
self.current_app.logger.info("AWS SNS request finished in {}".format(elapsed_time))
self.current_app.logger.info(
"AWS SNS request finished in {}".format(elapsed_time)
)
return response["MessageId"]
if not matched: