From 45b1b11abb1aa2dfd9fd76133ea1522f2dc02a0e Mon Sep 17 00:00:00 2001 From: Chris Hill-Scott Date: Thu, 20 Sep 2018 09:43:28 +0100 Subject: [PATCH 1/7] Refactor go-live readyness into service model MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit All this info is info about the service. So it’s better to put it on the service model because: - encapsulation - later reuse --- app/main/views/service_settings.py | 25 +--------- app/notify_client/models.py | 49 +++++++++++++++++++ app/templates/main_nav.html | 2 +- .../service-settings/request-to-go-live.html | 12 ++--- 4 files changed, 57 insertions(+), 31 deletions(-) diff --git a/app/main/views/service_settings.py b/app/main/views/service_settings.py index d25160b6d..98ff57e0b 100644 --- a/app/main/views/service_settings.py +++ b/app/main/views/service_settings.py @@ -180,30 +180,7 @@ def service_name_change_confirm(service_id): @user_has_permissions('manage_service') def request_to_go_live(service_id): return render_template( - 'views/service-settings/request-to-go-live.html', - has_team_members=( - user_api_client.get_count_of_users_with_permission( - service_id, 'manage_service' - ) > 1 - ), - has_templates=( - service_api_client.count_service_templates(service_id) > 0 - ), - has_email_templates=( - service_api_client.count_service_templates(service_id, template_type='email') > 0 - ), - has_sms_templates=( - service_api_client.count_service_templates(service_id, template_type='sms') > 0 - ), - has_email_reply_to_address=bool( - service_api_client.get_reply_to_email_addresses(service_id) - ), - shouldnt_use_govuk_as_sms_sender=( - current_service.organisation_type in {'local', 'nhs'} - ), - sms_sender_is_govuk=get_default_sms_sender( - service_api_client.get_sms_senders(service_id) - ) in {'GOVUK', 'None'}, + 'views/service-settings/request-to-go-live.html' ) diff --git a/app/notify_client/models.py b/app/notify_client/models.py index 03a97851c..6e429f0fa 100644 --- a/app/notify_client/models.py +++ b/app/notify_client/models.py @@ -3,6 +3,8 @@ from itertools import chain from flask import request, session from flask_login import AnonymousUserMixin, UserMixin +from app.utils import get_default_sms_sender + roles = { 'send_messages': ['send_texts', 'send_emails', 'send_letters'], 'manage_templates': ['manage_templates'], @@ -302,7 +304,54 @@ class Service(dict): def has_permission(self, permission): return permission in self.permissions + @property def has_jobs(self): # Can’t import at top-level because app isn’t yet initialised from app import job_api_client return job_api_client.has_jobs(self.id) + + @property + def has_team_members(self): + from app import user_api_client + return user_api_client.get_count_of_users_with_permission( + self.id, 'manage_service' + ) > 1 + + @property + def has_templates(self): + from app import service_api_client + return service_api_client.count_service_templates( + self.id + ) > 0 + + @property + def has_email_templates(self): + from app import service_api_client + return service_api_client.count_service_templates( + self.id, template_type='email' + ) > 0 + + @property + def has_sms_templates(self): + from app import service_api_client + return service_api_client.count_service_templates( + self.id, template_type='sms' + ) > 0 + + @property + def has_email_reply_to_address(self): + from app import service_api_client + return bool(service_api_client.get_reply_to_email_addresses( + self.id + )) + + @property + def shouldnt_use_govuk_as_sms_sender(self): + return self.organisation_type in {'local', 'nhs'} + + @property + def sms_sender_is_govuk(self): + from app import service_api_client + return get_default_sms_sender( + service_api_client.get_sms_senders(self.id) + ) in {'GOVUK', 'None'} diff --git a/app/templates/main_nav.html b/app/templates/main_nav.html index 2259a982d..543828537 100644 --- a/app/templates/main_nav.html +++ b/app/templates/main_nav.html @@ -9,7 +9,7 @@
  • Templates
  • {% if not current_user.has_permissions('view_activity') %}
  • Sent messages
  • - {% if current_service.has_jobs() %} + {% if current_service.has_jobs %}
  • Uploaded files
  • {% endif %} {% endif %} diff --git a/app/templates/views/service-settings/request-to-go-live.html b/app/templates/views/service-settings/request-to-go-live.html index 28cf3c503..0b92f1d48 100644 --- a/app/templates/views/service-settings/request-to-go-live.html +++ b/app/templates/views/service-settings/request-to-go-live.html @@ -16,30 +16,30 @@

    Before you request to go live

    {% call task_list_wrapper() %} {{ task_list_item( - has_team_members, + current_service.has_team_members, 'Add a team member who can manage settings, team and usage '.format( url_for('main.manage_users', service_id=current_service.id) )|safe, ) }} {{ task_list_item( - has_templates, + current_service.has_templates, 'Add templates with examples of the content you plan to send '.format( url_for('main.choose_template', service_id=current_service.id) )|safe, ) }} - {% if has_email_templates %} + {% if current_service.has_email_templates %} {{ task_list_item( - has_email_reply_to_address, + current_service.has_email_reply_to_address, 'Add an email reply-to address'.format( url_for('main.service_email_reply_to', service_id=current_service.id) )|safe, ) }} {% endif %} - {% if has_sms_templates and shouldnt_use_govuk_as_sms_sender %} + {% if current_service.has_sms_templates and current_service.shouldnt_use_govuk_as_sms_sender %} {{ task_list_item( - not sms_sender_is_govuk, + not current_service.sms_sender_is_govuk, 'Change your text message sender name'.format( url_for('main.service_sms_senders', service_id=current_service.id) )|safe From f29c6c90c072520a976f32486ec88a8bb96b46d5 Mon Sep 17 00:00:00 2001 From: Chris Hill-Scott Date: Thu, 20 Sep 2018 10:06:56 +0100 Subject: [PATCH 2/7] Add info about checklist to ticket So that someone picking up a ticket can be warned that they should be checking these things. --- app/main/views/service_settings.py | 2 + app/notify_client/models.py | 20 ++++ tests/app/main/views/test_service_settings.py | 112 +++++++++++++++++- 3 files changed, 132 insertions(+), 2 deletions(-) diff --git a/app/main/views/service_settings.py b/app/main/views/service_settings.py index 98ff57e0b..46d9a7319 100644 --- a/app/main/views/service_settings.py +++ b/app/main/views/service_settings.py @@ -199,6 +199,7 @@ def submit_request_to_go_live(service_id): '\n---' '\nOrganisation type: {organisation_type}' '\nAgreement signed: {agreement}' + '\nChecklist completed: {checklist}' '\nEmails in next year: {volume_email}' '\nText messages in next year: {volume_sms}' '\nLetters in next year: {volume_letter}' @@ -221,6 +222,7 @@ def submit_request_to_go_live(service_id): service_dashboard=url_for('main.service_dashboard', service_id=current_service.id, _external=True), organisation_type=str(current_service.organisation_type).title(), agreement=AgreementInfo.from_current_user().as_human_readable, + checklist='Yes' if current_service.go_live_checklist_completed else 'No', volume_email=form.volume_email.data, volume_sms=form.volume_sms.data, volume_letter=form.volume_letter.data, diff --git a/app/notify_client/models.py b/app/notify_client/models.py index 6e429f0fa..0a09e29b1 100644 --- a/app/notify_client/models.py +++ b/app/notify_client/models.py @@ -355,3 +355,23 @@ class Service(dict): return get_default_sms_sender( service_api_client.get_sms_senders(self.id) ) in {'GOVUK', 'None'} + + @property + def go_live_checklist_completed(self): + return all(( + self.has_team_members, + self.has_templates, + any(( + not self.has_email_templates, + self.has_email_reply_to_address, + )), + any(( + not self.has_sms_templates, + not self.shouldnt_use_govuk_as_sms_sender, + not self.sms_sender_is_govuk, + )) + )) + + @property + def go_live_checklist_completed_as_yes_no(self): + return 'Yes' if self.go_live_checklist_completed else 'No' diff --git a/tests/app/main/views/test_service_settings.py b/tests/app/main/views/test_service_settings.py index f35ca07ce..c3f48ea51 100644 --- a/tests/app/main/views/test_service_settings.py +++ b/tests/app/main/views/test_service_settings.py @@ -1,6 +1,6 @@ import uuid from functools import partial -from unittest.mock import ANY, call +from unittest.mock import ANY, PropertyMock, call from urllib.parse import parse_qs, urlparse import pytest @@ -737,7 +737,9 @@ def test_should_redirect_after_request_to_go_live( single_letter_contact_block, mock_get_service_organisation, single_sms_sender, - mock_get_service_settings_page_common + mock_get_service_settings_page_common, + mock_get_service_templates, + mock_get_users_by_service, ): mock_post = mocker.patch('app.main.views.service_settings.zendesk_client.create_ticket', autospec=True) page = client_request.post( @@ -765,6 +767,7 @@ def test_should_redirect_after_request_to_go_live( '---\n' 'Organisation type: Central\n' 'Agreement signed: Can’t tell (domain is user.gov.uk)\n' + 'Checklist completed: No\n' 'Emails in next year: 111\n' 'Text messages in next year: 222\n' 'Letters in next year: 333\n' @@ -782,6 +785,111 @@ def test_should_redirect_after_request_to_go_live( ) +@pytest.mark.parametrize( + ( + 'has_team_members,' + 'has_templates,' + 'has_email_templates,' + 'has_sms_templates,' + 'has_email_reply_to_address,' + 'shouldnt_use_govuk_as_sms_sender,' + 'sms_sender_is_govuk,' + 'expected,' + ), + ( + ( # Just sending email + True, + True, + True, + False, + True, + True, + True, + 'Yes', + ), + ( # Needs to set reply to address + True, + True, + True, + False, + False, + True, + True, + 'No', + ), + ( # Just sending SMS + True, + True, + False, + True, + True, + True, + False, + 'Yes', + ), + ( # Needs to change SMS sender + True, + True, + False, + True, + True, + True, + True, + 'No', + ), + ( # Needs team members + False, + True, + False, + True, + True, + True, + False, + 'No', + ), + ( # Needs templates + True, + False, + False, + True, + True, + True, + False, + 'No', + ), + ), +) +def test_ready_to_go_live( + client_request, + mocker, + has_team_members, + has_templates, + has_email_templates, + has_sms_templates, + has_email_reply_to_address, + shouldnt_use_govuk_as_sms_sender, + sms_sender_is_govuk, + expected, +): + for prop in { + 'has_team_members', + 'has_templates', + 'has_email_templates', + 'has_sms_templates', + 'has_email_reply_to_address', + 'shouldnt_use_govuk_as_sms_sender', + 'sms_sender_is_govuk', + }: + mocker.patch( + 'app.notify_client.models.Service.{}'.format(prop), + new_callable=PropertyMock + ).return_value = locals()[prop] + + assert app.notify_client.models.Service({ + 'id': fake_uuid() + }).go_live_checklist_completed_as_yes_no == expected + + @pytest.mark.parametrize('route', [ 'main.service_settings', 'main.service_name_change', From 6990128212dd72f65f04a3e387bf511e31682ecc Mon Sep 17 00:00:00 2001 From: Chris Hill-Scott Date: Thu, 20 Sep 2018 14:28:14 +0100 Subject: [PATCH 3/7] Bump utils to 30.3.0 Depends on: - [ ] https://github.com/alphagov/notifications-utils/pull/529 --- requirements-app.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-app.txt b/requirements-app.txt index 2b33e908e..f3a59c738 100644 --- a/requirements-app.txt +++ b/requirements-app.txt @@ -21,4 +21,4 @@ notifications-python-client==5.1.0 # PaaS awscli-cwlogs>=1.4,<1.5 -git+https://github.com/alphagov/notifications-utils.git@30.1.2#egg=notifications-utils==30.1.2 +git+https://github.com/alphagov/notifications-utils.git@30.3.0#egg=notifications-utils==30.3.0 From de65c30624fa420b1ce77a6d734c8f321a6f5a03 Mon Sep 17 00:00:00 2001 From: Chris Hill-Scott Date: Wed, 19 Sep 2018 17:34:26 +0100 Subject: [PATCH 4/7] Tag request to go live tickets automatically MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit At the moment we manually tag tickets as they come in so we can analyse how many of each type we’re getting. Further, we manually tag all the request to go live tickets once a month to analyse how many are complete/incomplete. All this tagging is useful, but quite time consuming. Notify already knows this information and – using the Zendesk API – we can tag them automatically. I’ve checked with Holly and this is the taxonomy we want to use. --- app/main/views/service_settings.py | 48 ++++++++++- tests/app/main/views/test_service_settings.py | 81 ++++++++++++++++++- 2 files changed, 123 insertions(+), 6 deletions(-) diff --git a/app/main/views/service_settings.py b/app/main/views/service_settings.py index 46d9a7319..0b9fc0702 100644 --- a/app/main/views/service_settings.py +++ b/app/main/views/service_settings.py @@ -222,7 +222,7 @@ def submit_request_to_go_live(service_id): service_dashboard=url_for('main.service_dashboard', service_id=current_service.id, _external=True), organisation_type=str(current_service.organisation_type).title(), agreement=AgreementInfo.from_current_user().as_human_readable, - checklist='Yes' if current_service.go_live_checklist_completed else 'No', + checklist=current_service.go_live_checklist_completed_as_yes_no, volume_email=form.volume_email.data, volume_sms=form.volume_sms.data, volume_letter=form.volume_letter.data, @@ -235,7 +235,8 @@ def submit_request_to_go_live(service_id): ), ticket_type=zendesk_client.TYPE_QUESTION, user_email=current_user.email_address, - user_name=current_user.name + user_name=current_user.name, + tags=get_request_to_go_live_tags(current_service, current_user), ) flash('Thanks for your request to go live. We’ll get back to you within one working day.', 'default') @@ -1106,3 +1107,46 @@ def check_contact_details_type(contact_details): return 'email_address' else: return 'phone_number' + + +def get_request_to_go_live_tags(service, user): + return list(_get_request_to_go_live_tags( + service, + AgreementInfo.from_user(user).agreement_signed, + )) + + +def _get_request_to_go_live_tags(service, agreement_signed): + + BASE = 'notify_request_to_go_live' + COMPLETE = BASE + '_complete' + INCOMPLETE = BASE + '_incomplete' + + yield BASE + + if service.go_live_checklist_completed and agreement_signed: + return COMPLETE + + yield INCOMPLETE + + if not service.go_live_checklist_completed: + yield INCOMPLETE + '_checklist' + + if not agreement_signed: + yield INCOMPLETE + '_mou' + + if service.has_email_templates and not service.has_email_reply_to_address: + yield INCOMPLETE + '_email_reply_to' + + if not service.has_team_members: + yield INCOMPLETE + '_team_member' + + if not service.has_templates: + yield INCOMPLETE + '_template_content' + + if ( + service.has_sms_templates and + service.shouldnt_use_govuk_as_sms_sender and + service.sms_sender_is_govuk + ): + yield INCOMPLETE + '_sms_sender' diff --git a/tests/app/main/views/test_service_settings.py b/tests/app/main/views/test_service_settings.py index c3f48ea51..5be558213 100644 --- a/tests/app/main/views/test_service_settings.py +++ b/tests/app/main/views/test_service_settings.py @@ -758,7 +758,14 @@ def test_should_redirect_after_request_to_go_live( message=ANY, ticket_type=ZendeskClient.TYPE_QUESTION, user_name=active_user_with_permissions.name, - user_email=active_user_with_permissions.email_address + user_email=active_user_with_permissions.email_address, + tags=[ + 'notify_request_to_go_live', + 'notify_request_to_go_live_incomplete', + 'notify_request_to_go_live_incomplete_checklist', + 'notify_request_to_go_live_incomplete_mou', + 'notify_request_to_go_live_incomplete_team_member', + ], ) assert mock_post.call_args[1]['message'] == ( 'Service: service one\n' @@ -794,7 +801,9 @@ def test_should_redirect_after_request_to_go_live( 'has_email_reply_to_address,' 'shouldnt_use_govuk_as_sms_sender,' 'sms_sender_is_govuk,' - 'expected,' + 'expected_readyness,' + 'agreement_signed,' + 'expected_tags,' ), ( ( # Just sending email @@ -806,6 +815,10 @@ def test_should_redirect_after_request_to_go_live( True, True, 'Yes', + True, + [ + 'notify_request_to_go_live', + ], ), ( # Needs to set reply to address True, @@ -816,6 +829,13 @@ def test_should_redirect_after_request_to_go_live( True, True, 'No', + True, + [ + 'notify_request_to_go_live', + 'notify_request_to_go_live_incomplete', + 'notify_request_to_go_live_incomplete_checklist', + 'notify_request_to_go_live_incomplete_email_reply_to', + ], ), ( # Just sending SMS True, @@ -826,6 +846,10 @@ def test_should_redirect_after_request_to_go_live( True, False, 'Yes', + True, + [ + 'notify_request_to_go_live', + ], ), ( # Needs to change SMS sender True, @@ -836,6 +860,13 @@ def test_should_redirect_after_request_to_go_live( True, True, 'No', + True, + [ + 'notify_request_to_go_live', + 'notify_request_to_go_live_incomplete', + 'notify_request_to_go_live_incomplete_checklist', + 'notify_request_to_go_live_incomplete_sms_sender', + ], ), ( # Needs team members False, @@ -846,6 +877,13 @@ def test_should_redirect_after_request_to_go_live( True, False, 'No', + True, + [ + 'notify_request_to_go_live', + 'notify_request_to_go_live_incomplete', + 'notify_request_to_go_live_incomplete_checklist', + 'notify_request_to_go_live_incomplete_team_member', + ], ), ( # Needs templates True, @@ -856,6 +894,34 @@ def test_should_redirect_after_request_to_go_live( True, False, 'No', + True, + [ + 'notify_request_to_go_live', + 'notify_request_to_go_live_incomplete', + 'notify_request_to_go_live_incomplete_checklist', + 'notify_request_to_go_live_incomplete_template_content', + ], + ), + ( # Everything is wrong + False, + False, + True, + True, + False, + True, + True, + 'No', + False, + [ + 'notify_request_to_go_live', + 'notify_request_to_go_live_incomplete', + 'notify_request_to_go_live_incomplete_checklist', + 'notify_request_to_go_live_incomplete_mou', + 'notify_request_to_go_live_incomplete_email_reply_to', + 'notify_request_to_go_live_incomplete_team_member', + 'notify_request_to_go_live_incomplete_template_content', + 'notify_request_to_go_live_incomplete_sms_sender', + ], ), ), ) @@ -869,7 +935,9 @@ def test_ready_to_go_live( has_email_reply_to_address, shouldnt_use_govuk_as_sms_sender, sms_sender_is_govuk, - expected, + expected_readyness, + agreement_signed, + expected_tags, ): for prop in { 'has_team_members', @@ -887,7 +955,12 @@ def test_ready_to_go_live( assert app.notify_client.models.Service({ 'id': fake_uuid() - }).go_live_checklist_completed_as_yes_no == expected + }).go_live_checklist_completed_as_yes_no == expected_readyness + + assert list(app.main.views.service_settings._get_request_to_go_live_tags( + app.notify_client.models.Service({'id': fake_uuid()}), + agreement_signed, + )) == expected_tags @pytest.mark.parametrize('route', [ From f29cfc0d485cc031e9eeaee4d0bd2ad629b362de Mon Sep 17 00:00:00 2001 From: Chris Hill-Scott Date: Fri, 21 Sep 2018 09:01:58 +0100 Subject: [PATCH 5/7] Auto-tag email branding requests --- app/main/views/service_settings.py | 1 + tests/app/main/views/test_service_settings.py | 1 + 2 files changed, 2 insertions(+) diff --git a/app/main/views/service_settings.py b/app/main/views/service_settings.py index 0b9fc0702..fa779ccc7 100644 --- a/app/main/views/service_settings.py +++ b/app/main/views/service_settings.py @@ -1022,6 +1022,7 @@ def branding_request(service_id): ticket_type=zendesk_client.TYPE_QUESTION, user_email=current_user.email_address, user_name=current_user.name, + tags=['notify_action_add_branding'], ) flash(( diff --git a/tests/app/main/views/test_service_settings.py b/tests/app/main/views/test_service_settings.py index 5be558213..9052439ce 100644 --- a/tests/app/main/views/test_service_settings.py +++ b/tests/app/main/views/test_service_settings.py @@ -3203,6 +3203,7 @@ def test_submit_email_branding_request( ticket_type='question', user_email='test@user.gov.uk', user_name='Test User', + tags=['notify_action_add_branding'], ) assert normalize_spaces(page.select_one('.banner-default').text) == ( 'Thanks for your branding request. We’ll get back to you ' From 8bb23e09f2833eb81f1b67dd4d8b19677f49a1d9 Mon Sep 17 00:00:00 2001 From: Chris Hill-Scott Date: Fri, 21 Sep 2018 09:13:12 +0100 Subject: [PATCH 6/7] Refactor to reduce nesting and repetition --- app/main/views/service_settings.py | 32 ++++++++++-------------------- app/notify_client/models.py | 23 ++++++++++++--------- 2 files changed, 24 insertions(+), 31 deletions(-) diff --git a/app/main/views/service_settings.py b/app/main/views/service_settings.py index fa779ccc7..58b84b442 100644 --- a/app/main/views/service_settings.py +++ b/app/main/views/service_settings.py @@ -1128,26 +1128,14 @@ def _get_request_to_go_live_tags(service, agreement_signed): if service.go_live_checklist_completed and agreement_signed: return COMPLETE - yield INCOMPLETE - - if not service.go_live_checklist_completed: - yield INCOMPLETE + '_checklist' - - if not agreement_signed: - yield INCOMPLETE + '_mou' - - if service.has_email_templates and not service.has_email_reply_to_address: - yield INCOMPLETE + '_email_reply_to' - - if not service.has_team_members: - yield INCOMPLETE + '_team_member' - - if not service.has_templates: - yield INCOMPLETE + '_template_content' - - if ( - service.has_sms_templates and - service.shouldnt_use_govuk_as_sms_sender and - service.sms_sender_is_govuk + for test, tag in ( + (True, ''), + (not service.go_live_checklist_completed, '_checklist'), + (not agreement_signed, '_mou'), + (service.needs_to_add_email_reply_to_address, '_email_reply_to'), + (not service.has_team_members, '_team_member'), + (not service.has_templates, '_template_content'), + (service.needs_to_change_sms_sender, '_sms_sender'), ): - yield INCOMPLETE + '_sms_sender' + if test: + yield INCOMPLETE + tag diff --git a/app/notify_client/models.py b/app/notify_client/models.py index 0a09e29b1..356a00395 100644 --- a/app/notify_client/models.py +++ b/app/notify_client/models.py @@ -345,6 +345,10 @@ class Service(dict): self.id )) + @property + def needs_to_add_email_reply_to_address(self): + return self.has_email_templates and not self.has_email_reply_to_address + @property def shouldnt_use_govuk_as_sms_sender(self): return self.organisation_type in {'local', 'nhs'} @@ -356,20 +360,21 @@ class Service(dict): service_api_client.get_sms_senders(self.id) ) in {'GOVUK', 'None'} + @property + def needs_to_change_sms_sender(self): + return all(( + self.has_sms_templates, + self.shouldnt_use_govuk_as_sms_sender, + self.sms_sender_is_govuk, + )) + @property def go_live_checklist_completed(self): return all(( self.has_team_members, self.has_templates, - any(( - not self.has_email_templates, - self.has_email_reply_to_address, - )), - any(( - not self.has_sms_templates, - not self.shouldnt_use_govuk_as_sms_sender, - not self.sms_sender_is_govuk, - )) + not self.needs_to_add_email_reply_to_address, + not self.needs_to_change_sms_sender, )) @property From 073aaa0db736ed25dc380b83cd3114b25bdf52c9 Mon Sep 17 00:00:00 2001 From: Chris Hill-Scott Date: Fri, 21 Sep 2018 09:30:38 +0100 Subject: [PATCH 7/7] Freeze requirements --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 224f8f34a..3afa9cc7f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -23,7 +23,7 @@ notifications-python-client==5.1.0 # PaaS awscli-cwlogs>=1.4,<1.5 -git+https://github.com/alphagov/notifications-utils.git@30.1.2#egg=notifications-utils==30.1.2 +git+https://github.com/alphagov/notifications-utils.git@30.3.0#egg=notifications-utils==30.3.0 ## The following requirements were added by pip freeze: awscli==1.16.19