From fc8d5abf99898a2a5d4adc40cad08e787876b5c7 Mon Sep 17 00:00:00 2001 From: irfanuddinahmad Date: Fri, 26 Feb 2021 20:47:08 +0500 Subject: [PATCH] Updated Braze client for non edx users --- ecommerce_worker/braze/v1/client.py | 141 ++++++++++++++---- .../braze/v1/tests/test_client.py | 63 +++++--- ecommerce_worker/configuration/base.py | 3 + ecommerce_worker/configuration/test.py | 1 + ecommerce_worker/sailthru/v1/tasks.py | 12 +- .../sailthru/v1/tests/test_tasks.py | 25 ++++ setup.py | 2 +- 7 files changed, 191 insertions(+), 56 deletions(-) diff --git a/ecommerce_worker/braze/v1/client.py b/ecommerce_worker/braze/v1/client.py index 409eb515..455a70c9 100644 --- a/ecommerce_worker/braze/v1/client.py +++ b/ecommerce_worker/braze/v1/client.py @@ -6,7 +6,7 @@ import requests -from urllib.parse import urlencode +from urllib.parse import urlencode, urljoin from celery.utils.log import get_task_logger @@ -33,13 +33,12 @@ def get_braze_configuration(site_code): return config -def get_braze_client(site_code, endpoint=None): +def get_braze_client(site_code): """ Returns a Braze client for the specified site. Arguments: site_code (str): Site for which the client should be configured. - endpoint (str): The endpoint for the API e.g. /messages/send or /email/hard_bounces Returns: BrazeClient @@ -60,25 +59,31 @@ def get_braze_client(site_code, endpoint=None): rest_api_key = config.get('BRAZE_REST_API_KEY') webapp_api_key = config.get('BRAZE_WEBAPP_API_KEY') rest_api_url = config.get('REST_API_URL') - message_endpoint = endpoint or config.get('MESSAGES_SEND_ENDPOINT') + messages_send_endpoint = config.get('MESSAGES_SEND_ENDPOINT') + email_bounce_endpoint = config.get('EMAIL_BOUNCE_ENDPOINT') + new_alias_endpoint = config.get('NEW_ALIAS_ENDPOINT') + users_track_endpoint = config.get('USERS_TRACK_ENDPOINT') from_email = config.get('FROM_EMAIL') + email_template_id = config.get('EMAIL_TEMPLATE_ID') if ( not rest_api_key or - not webapp_api_key or - not rest_api_url or - not message_endpoint or - not from_email + not webapp_api_key ): - msg = 'Required parameters missing for site {}'.format(site_code) + msg = 'Required keys missing for site {}'.format(site_code) log.error(msg) raise ConfigurationError(msg) return BrazeClient( rest_api_key=rest_api_key, webapp_api_key=webapp_api_key, - request_url=rest_api_url + message_endpoint, - from_email=from_email + rest_api_url=rest_api_url, + messages_send_endpoint=messages_send_endpoint, + email_bounce_endpoint=email_bounce_endpoint, + new_alias_endpoint=new_alias_endpoint, + users_track_endpoint=users_track_endpoint, + from_email=from_email, + email_template_id=email_template_id ) @@ -87,34 +92,56 @@ class BrazeClient(object): Client for Braze REST API """ - def __init__(self, rest_api_key, webapp_api_key, request_url, from_email): + def __init__( + self, + rest_api_key, + webapp_api_key, + rest_api_url, + messages_send_endpoint, + email_bounce_endpoint, + new_alias_endpoint, + users_track_endpoint, + from_email, + email_template_id + ): """ Initialize the Braze Client with configuration values. Arguments: rest_api_key (str): API key to use for accessing Braze REST endpoints webapp_api_key (str): Key that identifies the Braze webapp application - request_url (str): Braze endpoint url + rest_api_url (str): REST API url, + messages_send_endpoint (str): Messages send endpoint, + email_bounce_endpoint (str): Email bounce endpoint, + new_alias_endpoint (str): New alias endpoint, + users_track_endpoint (str): Users track endpoint, from_email (str): Braze email from address + email_template_id (str): Braze email template identifier """ self.rest_api_key = rest_api_key self.webapp_api_key = webapp_api_key + self.rest_api_url = rest_api_url + self.messages_send_endpoint = messages_send_endpoint + self.email_bounce_endpoint = email_bounce_endpoint + self.new_alias_endpoint = new_alias_endpoint + self.users_track_endpoint = users_track_endpoint self.from_email = from_email + self.email_template_id = email_template_id self.session = requests.Session() - self.request_url = request_url - def __create_post_request(self, body): + def __create_post_request(self, body, endpoint): """ Creates a request and returns a response. Arguments: body (dict): The request body + endpoint (str): The endpoint for the API e.g. /messages/send or /email/hard_bounces Returns: response (dict): The response object """ response = {'errors': []} - r = self._post_request(body) + r = self._post_request(body, endpoint) response.update(r.json()) response['status_code'] = r.status_code @@ -126,12 +153,13 @@ def __create_post_request(self, body): raise BrazeClientError(message, response['errors']) return response - def _post_request(self, body): + def _post_request(self, body, endpoint): """ Http posts the message body with associated headers. Arguments: body (dict): The request body + endpoint (str): The endpoint for the API e.g. /messages/send or /email/hard_bounces Returns: r (requests.Response): The http response object @@ -139,7 +167,7 @@ def _post_request(self, body): self.session.headers.update( {'Authorization': u'Bearer {}'.format(self.rest_api_key), 'Content-Type': 'application/json'} ) - r = self.session.post(self.request_url, json=body, timeout=2) + r = self.session.post(urljoin(self.rest_api_url, endpoint), json=body, timeout=2) if r.status_code == 429: reset_epoch_s = float(r.headers.get('X-RateLimit-Reset', 0)) raise BrazeRateLimitError(reset_epoch_s) @@ -147,18 +175,19 @@ def _post_request(self, body): raise BrazeInternalServerError return r - def __create_get_request(self, parameters): + def __create_get_request(self, parameters, endpoint): """ Creates a request and returns a response. Arguments: parameters (dict): The request parameters + endpoint (str): The endpoint for the API e.g. /messages/send or /email/hard_bounces Returns: response (dict): The response object """ response = {'errors': []} - r = self._get_request(parameters) + r = self._get_request(parameters, endpoint) response.update(r.json()) response['status_code'] = r.status_code message = response["message"] @@ -169,12 +198,13 @@ def __create_get_request(self, parameters): raise BrazeClientError(message, response['errors']) return response - def _get_request(self, parameters): + def _get_request(self, parameters, endpoint): """ Http GET the parameters with associated headers. Arguments: parameters (dict): The request parameters + endpoint (str): The endpoint for the API e.g. /messages/send or /email/hard_bounces Returns: r (requests.Response): The http response object @@ -182,7 +212,7 @@ def _get_request(self, parameters): self.session.headers.update( {'Authorization': u'Bearer {}'.format(self.rest_api_key), 'Content-Type': 'application/json'} ) - url_with_parameters = self.request_url + '?' + urlencode(parameters) + url_with_parameters = urljoin(self.rest_api_url, endpoint) + '?' + urlencode(parameters) r = self.session.get(url_with_parameters) if r.status_code == 429: reset_epoch_s = float(r.headers.get('X-RateLimit-Reset', 0)) @@ -191,6 +221,56 @@ def _get_request(self, parameters): raise BrazeInternalServerError return r + def create_braze_alias(self, recipient_emails): + """ + Creates a Braze anonymous user and assigns it the recipient email address. + + Alias naming format: + { + "attributes": [ + { + "user_alias" : { + "alias_name" : "Enterprise", + "alias_label" : "someuser@someorg.org" + }, + "email" : "someuser@someorg.org" + } + ] + } + + Arguments: + recipient_emails (list): e.g. ['test1@example.com', 'test2@example.com'] + + """ + if not recipient_emails: + raise BrazeClientError("Missing parameters for Alias creation") + user_aliases = [] + attributes = [] + for recipient_email in recipient_emails: + user_alias = { + 'alias_name': 'Enterprise', + 'alias_label': recipient_email + } + user_aliases.append(user_alias) + attribute = { + 'user_alias': { + 'alias_name': 'Enterprise', + 'alias_label': recipient_email + }, + 'email': recipient_email + } + attributes.append(attribute) + + alias_message = { + 'user_aliases': user_aliases, + } + self.__create_post_request(alias_message, self.new_alias_endpoint) + + attribute_message = { + 'attributes': attributes + } + self.__create_post_request(attribute_message, self.users_track_endpoint) + def send_message( self, email_ids, @@ -232,20 +312,29 @@ def send_message( """ if not email_ids or not subject or not body: raise BrazeClientError("Missing parameters for Braze email") + self.create_braze_alias(email_ids) + user_aliases = [] + for email_id in email_ids: + user_alias = { + 'alias_name': 'Enterprise', + 'alias_label': email_id + } + user_aliases.append(user_alias) email = { 'app_id': self.webapp_api_key, 'subject': subject, 'from': sender_alias + self.from_email, - 'body': body + 'body': body, + 'email_template_id': self.email_template_id } message = { - 'external_user_ids': email_ids, + 'user_aliases': user_aliases, 'messages': { 'email': email } } - return self.__create_post_request(message) + return self.__create_post_request(message, self.messages_send_endpoint) def did_email_bounce( self, @@ -266,7 +355,7 @@ def did_email_bounce( 'email': email_id } - response = self.__create_get_request(parameters) + response = self.__create_get_request(parameters, self.email_bounce_endpoint) if response['emails']: return True diff --git a/ecommerce_worker/braze/v1/tests/test_client.py b/ecommerce_worker/braze/v1/tests/test_client.py index 4dd21728..b134c80a 100644 --- a/ecommerce_worker/braze/v1/tests/test_client.py +++ b/ecommerce_worker/braze/v1/tests/test_client.py @@ -37,12 +37,30 @@ class BrazeClientTests(TestCase): 'BRAZE_WEBAPP_API_KEY': 'webapp_api_key', 'REST_API_URL': 'https://rest.iad-06.braze.com', 'MESSAGES_SEND_ENDPOINT': '/messages/send', + 'EMAIL_BOUNCE_ENDPOINT': '/email/hard_bounces', + 'NEW_ALIAS_ENDPOINT': '/users/alias/new', + 'USERS_TRACK_ENDPOINT': '/users/track', 'FROM_EMAIL': '', } } } - SEND_ENDPOINT = '/messages/send' - BOUNCE_ENDPOINT = '/email/hard_bounces' + + def mock_braze_user_endpoints(self): + """ Mock POST requests to the user alias and track endpoints. """ + host = 'https://rest.iad-06.braze.com/users/track' + responses.add( + responses.POST, + host, + json={'message': 'success'}, + status=201 + ) + host = 'https://rest.iad-06.braze.com/users/alias/new' + responses.add( + responses.POST, + host, + json={'message': 'success'}, + status=201 + ) def assert_get_braze_client_raises(self, exc_class, config): """ @@ -50,7 +68,7 @@ def assert_get_braze_client_raises(self, exc_class, config): """ with patch('ecommerce_worker.braze.v1.client.get_braze_configuration', Mock(return_value=config)): with self.assertRaises(exc_class): - get_braze_client(self.SITE_CODE, self.SEND_ENDPOINT) + get_braze_client(self.SITE_CODE) def test_get_braze_client_with_braze_disabled(self): """ @@ -64,16 +82,8 @@ def test_get_braze_client_with_braze_disabled(self): @ddt.data( {}, - {'BRAZE_REST_API_KEY': None, 'BRAZE_WEBAPP_API_KEY': None, - 'REST_API_URL': None, 'MESSAGES_SEND_ENDPOINT': None, 'FROM_EMAIL': None}, - {'BRAZE_REST_API_KEY': 'test', 'BRAZE_WEBAPP_API_KEY': None, - 'REST_API_URL': 'test', 'MESSAGES_SEND_ENDPOINT': 'test', 'FROM_EMAIL': 'test'}, - {'BRAZE_REST_API_KEY': None, 'BRAZE_WEBAPP_API_KEY': 'test', - 'REST_API_URL': 'test', 'MESSAGES_SEND_ENDPOINT': 'test', 'FROM_EMAIL': 'test'}, - {'BRAZE_REST_API_KEY': 'test', 'BRAZE_WEBAPP_API_KEY': 'test', - 'REST_API_URL': None, 'MESSAGES_SEND_ENDPOINT': 'test', 'FROM_EMAIL': 'test'}, - {'BRAZE_REST_API_KEY': 'test', 'BRAZE_WEBAPP_API_KEY': 'test', - 'REST_API_URL': 'test', 'MESSAGES_SEND_ENDPOINT': 'test', 'FROM_EMAIL': None}, + {'BRAZE_REST_API_KEY': None, 'BRAZE_WEBAPP_API_KEY': None}, + {'BRAZE_REST_API_KEY': 'test', 'BRAZE_WEBAPP_API_KEY': None}, ) def test_get_braze_client_without_credentials(self, braze_config): """ @@ -84,13 +94,24 @@ def test_get_braze_client_without_credentials(self, braze_config): with mock.patch('ecommerce_worker.braze.v1.client.log.error') as mock_log: self.assert_get_braze_client_raises(ConfigurationError, braze_config) - mock_log.assert_called_once_with('Required parameters missing for site {}'.format(self.SITE_CODE)) + mock_log.assert_called_once_with('Required keys missing for site {}'.format(self.SITE_CODE)) + + def test_create_braze_alias(self): + """ + Asserts an error is raised by a call to create_braze_alias. + """ + braze = self.BRAZE_OVERRIDES[self.SITE_CODE]['BRAZE'] + with patch('ecommerce_worker.braze.v1.client.get_braze_configuration', Mock(return_value=braze)): + client = get_braze_client(self.SITE_CODE) + with self.assertRaises(BrazeClientError): + client.create_braze_alias(recipient_emails=[]) @responses.activate def test_send_braze_message_success(self): """ Verify that an email message is sent via BrazeClient. """ + self.mock_braze_user_endpoints() success_response = { 'dispatch_id': '66cdc28f8f082bc3074c0c79f', 'errors': [], @@ -98,7 +119,6 @@ def test_send_braze_message_success(self): 'status_code': 201 } host = 'https://rest.iad-06.braze.com/messages/send' - responses.add( responses.POST, host, @@ -107,7 +127,7 @@ def test_send_braze_message_success(self): ) braze = self.BRAZE_OVERRIDES[self.SITE_CODE]['BRAZE'] with patch('ecommerce_worker.braze.v1.client.get_braze_configuration', Mock(return_value=braze)): - client = get_braze_client(self.SITE_CODE, self.SEND_ENDPOINT) + client = get_braze_client(self.SITE_CODE) response = client.send_message( ['test1@example.com', 'test2@example.com'], 'Test Subject', @@ -126,6 +146,7 @@ def test_send_braze_message_failure(self, status_code, error): """ Verify that a failed email message throws the relevant error. """ + self.mock_braze_user_endpoints() failure_response = { 'message': 'Not a Success', 'status_code': status_code @@ -140,7 +161,7 @@ def test_send_braze_message_failure(self, status_code, error): ) braze = self.BRAZE_OVERRIDES[self.SITE_CODE]['BRAZE'] with patch('ecommerce_worker.braze.v1.client.get_braze_configuration', Mock(return_value=braze)): - client = get_braze_client(self.SITE_CODE, self.SEND_ENDPOINT) + client = get_braze_client(self.SITE_CODE) with self.assertRaises(error): response = client.send_message( ['test1@example.com', 'test2@example.com'], @@ -160,7 +181,7 @@ def test_send_braze_message_failure_missing_parameters(self, email, subject, bod """ braze = self.BRAZE_OVERRIDES[self.SITE_CODE]['BRAZE'] with patch('ecommerce_worker.braze.v1.client.get_braze_configuration', Mock(return_value=braze)): - client = get_braze_client(self.SITE_CODE, self.SEND_ENDPOINT) + client = get_braze_client(self.SITE_CODE) with self.assertRaises(BrazeClientError): response = client.send_message(email, subject, body) @@ -204,7 +225,7 @@ def test_bounced_email(self, response, did_bounce): ) braze = self.BRAZE_OVERRIDES[self.SITE_CODE]['BRAZE'] with patch('ecommerce_worker.braze.v1.client.get_braze_configuration', Mock(return_value=braze)): - client = get_braze_client(self.SITE_CODE, self.BOUNCE_ENDPOINT) + client = get_braze_client(self.SITE_CODE) bounce = client.did_email_bounce(bounced_email) self.assertEqual(bounce, did_bounce) @@ -235,7 +256,7 @@ def test_bounce_message_failure(self, status_code, error): ) braze = self.BRAZE_OVERRIDES[self.SITE_CODE]['BRAZE'] with patch('ecommerce_worker.braze.v1.client.get_braze_configuration', Mock(return_value=braze)): - client = get_braze_client(self.SITE_CODE, self.BOUNCE_ENDPOINT) + client = get_braze_client(self.SITE_CODE) with self.assertRaises(error): client.did_email_bounce(bounced_email) @@ -245,6 +266,6 @@ def test_did_email_bounce_failure_missing_parameters(self): """ braze = self.BRAZE_OVERRIDES[self.SITE_CODE]['BRAZE'] with patch('ecommerce_worker.braze.v1.client.get_braze_configuration', Mock(return_value=braze)): - client = get_braze_client(self.SITE_CODE, self.BOUNCE_ENDPOINT) + client = get_braze_client(self.SITE_CODE) with self.assertRaises(BrazeClientError): client.did_email_bounce(email_id=None) diff --git a/ecommerce_worker/configuration/base.py b/ecommerce_worker/configuration/base.py index cf5cbd62..77a1c8a7 100644 --- a/ecommerce_worker/configuration/base.py +++ b/ecommerce_worker/configuration/base.py @@ -76,9 +76,12 @@ 'BRAZE_ENABLE': False, 'BRAZE_REST_API_KEY': None, 'BRAZE_WEBAPP_API_KEY': None, + 'EMAIL_TEMPLATE_ID': '', 'REST_API_URL': 'https://rest.iad-06.braze.com', 'MESSAGES_SEND_ENDPOINT': '/messages/send', 'EMAIL_BOUNCE_ENDPOINT': '/email/hard_bounces', + 'NEW_ALIAS_ENDPOINT': '/users/alias/new', + 'USERS_TRACK_ENDPOINT': '/users/track', 'FROM_EMAIL': '', # Retry settings for Braze celery tasks 'BRAZE_RETRY_SECONDS': 3600, diff --git a/ecommerce_worker/configuration/test.py b/ecommerce_worker/configuration/test.py index 646dbbc2..d4c23790 100644 --- a/ecommerce_worker/configuration/test.py +++ b/ecommerce_worker/configuration/test.py @@ -49,6 +49,7 @@ 'BRAZE_ENABLE': False, 'BRAZE_REST_API_KEY': 'rest_api_key', 'BRAZE_WEBAPP_API_KEY': 'webapp_api_key', + 'EMAIL_TEMPLATE_ID': 'email_template_id', }) # Sailthru support unit test settings with override diff --git a/ecommerce_worker/sailthru/v1/tasks.py b/ecommerce_worker/sailthru/v1/tasks.py index b146c29d..e3e2e8c5 100644 --- a/ecommerce_worker/sailthru/v1/tasks.py +++ b/ecommerce_worker/sailthru/v1/tasks.py @@ -379,9 +379,8 @@ def _send_offer_assignment_email_via_braze(self, user_email, offer_assignment_id sender_alias (str): Enterprise Customer sender alias used as From Name. """ config = get_braze_configuration(site_code) - endpoint = config.get('MESSAGES_SEND_ENDPOINT') try: - braze_client = get_braze_client(site_code, endpoint) + braze_client = get_braze_client(site_code) response = braze_client.send_message( email_ids=list(user_email), subject=subject, @@ -532,9 +531,8 @@ def _send_offer_update_email_via_braze(self, user_email, subject, email_body, se sender_alias (str): Enterprise Customer sender alias used as From Name. """ config = get_braze_configuration(site_code) - endpoint = config.get('MESSAGES_SEND_ENDPOINT') try: - braze_client = get_braze_client(site_code, endpoint) + braze_client = get_braze_client(site_code) braze_client.send_message( email_ids=list(user_email), subject=subject, @@ -614,9 +612,8 @@ def _send_offer_usage_email_via_braze(self, emails, subject, email_body, site_co site_code (str): Identifier of the site sending the email. """ config = get_braze_configuration(site_code) - endpoint = config.get('MESSAGES_SEND_ENDPOINT') try: - braze_client = get_braze_client(site_code, endpoint) + braze_client = get_braze_client(site_code) braze_client.send_message( email_ids=list(emails), subject=subject, @@ -692,9 +689,8 @@ def _send_code_assignment_nudge_email_via_braze(self, email, subject, email_body site_code (str): Identifier of the site sending the email. """ config = get_braze_configuration(site_code) - endpoint = config.get('MESSAGES_SEND_ENDPOINT') try: - braze_client = get_braze_client(site_code, endpoint) + braze_client = get_braze_client(site_code) braze_client.send_message( email_ids=list(email), subject=subject, diff --git a/ecommerce_worker/sailthru/v1/tests/test_tasks.py b/ecommerce_worker/sailthru/v1/tests/test_tasks.py index b7681c52..92219f25 100644 --- a/ecommerce_worker/sailthru/v1/tests/test_tasks.py +++ b/ecommerce_worker/sailthru/v1/tests/test_tasks.py @@ -1022,6 +1022,9 @@ class SendOfferEmailsTestsWithBraze(TestCase): 'BRAZE_WEBAPP_API_KEY': 'webapp_api_key', 'REST_API_URL': 'https://rest.iad-06.braze.com', 'MESSAGES_SEND_ENDPOINT': '/messages/send', + 'EMAIL_BOUNCE_ENDPOINT': '/email/hard_bounces', + 'NEW_ALIAS_ENDPOINT': '/users/alias/new', + 'USERS_TRACK_ENDPOINT': '/users/track', 'FROM_EMAIL': '', 'BRAZE_RETRY_SECONDS': 3600, 'BRAZE_RETRY_ATTEMPTS': 6, @@ -1079,6 +1082,23 @@ def mock_ecommerce_assignmentemail_api(self, body, status=200): body=json.dumps(body), content_type='application/json', ) + def mock_braze_user_endpoints(self): + """ Mock POST requests to the user alias and track endpoints. """ + host = 'https://rest.iad-06.braze.com/users/track' + responses.add( + responses.POST, + host, + json={'message': 'success'}, + status=201 + ) + host = 'https://rest.iad-06.braze.com/users/alias/new' + responses.add( + responses.POST, + host, + json={'message': 'success'}, + status=201 + ) + @patch('ecommerce_worker.sailthru.v1.tasks.get_braze_client', Mock(side_effect=BrazeError)) @ddt.data( (send_offer_assignment_email, ASSIGNMENT_TASK_KWARGS, "Offer Assignment", 'assignment'), @@ -1138,6 +1158,7 @@ def test_api_client_error(self, task, task_kwargs, logger_prefix, log_message, @ddt.unpack def test_api_429_error_with_retry(self, task, task_kwargs): """ Verify the task is rescheduled if an API error occurs, and the request can be retried. """ + self.mock_braze_user_endpoints() failure_response = { 'message': 'Not a Success', 'status_code': 429 @@ -1166,6 +1187,7 @@ def test_api_429_error_with_retry(self, task, task_kwargs): @ddt.unpack def test_api_500_error_with_retry(self, task, task_kwargs): """ Verify 500 error triggers a request retry. """ + self.mock_braze_user_endpoints() failure_response = { 'message': 'Not a Success', 'status_code': 500 @@ -1195,6 +1217,7 @@ def test_api(self, task, task_kwargs): """ Test the happy path. """ + self.mock_braze_user_endpoints() success_response = { "dispatch_id": "66cdc28f8f082bc3074c0c79f", "errors": [], @@ -1217,6 +1240,7 @@ def test_api(self, task, task_kwargs): @patch('ecommerce_worker.sailthru.v1.tasks._update_assignment_email_status') def test_message_sent(self, mock_update_assignment): """ Verify a message is logged after a successful API call to send the message. """ + self.mock_braze_user_endpoints() success_response = { 'dispatch_id': '66cdc28f8f082bc3074c0c79f', 'errors': [], @@ -1245,6 +1269,7 @@ def test_message_sent(self, mock_update_assignment): @patch('ecommerce_worker.utils.get_ecommerce_client') def test_update_assignment_exception(self, mock_get_ecommerce_client): """ Verify a message is logged after an unsuccessful API call to update the status. """ + self.mock_braze_user_endpoints() success_response = { 'dispatch_id': '66cdc28f8f082bc3074c0c79f', 'errors': [], diff --git a/setup.py b/setup.py index c18f9b2d..f8771ea5 100644 --- a/setup.py +++ b/setup.py @@ -37,7 +37,7 @@ def is_requirement(line): setup( name='edx-ecommerce-worker', - version='1.1.8', + version='1.1.9', description='Celery tasks supporting the operations of edX\'s ecommerce service', long_description=long_description, classifiers=[