diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 80d1772..ae29392 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -14,6 +14,10 @@ Change Log Unreleased ~~~~~~~~~~ +[0.2.4] +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +feat: adds 'create_recipients' function to the braze client + [0.2.3] ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ feat: pass error response content into raised exceptions diff --git a/braze/client.py b/braze/client.py index 63b640f..7e4448f 100644 --- a/braze/client.py +++ b/braze/client.py @@ -11,6 +11,7 @@ from braze.constants import ( GET_EXTERNAL_IDS_CHUNK_SIZE, + MAX_NUM_IDENTIFY_USERS_ALIASES, REQUEST_TYPE_GET, REQUEST_TYPE_POST, TRACK_USER_COMPONENT_CHUNK_SIZE, @@ -209,6 +210,96 @@ def identify_users(self, aliases_to_identify): return self._make_request(payload, BrazeAPIEndpoints.IDENTIFY_USERS, REQUEST_TYPE_POST) + def create_recipients(self, alias_label, user_id_by_email, trigger_properties_by_email=None): + """ + Create a recipient object using the dictionary, `user_id_by_email` + containing the user_email key and `lms_user_id` value. + Identifies a list of given email addresess with any existing Braze alias records + via the provided ``lms_user_id``. + + https://www.braze.com/docs/api/objects_filters/user_alias_object + The user_alias objects requires a passed in alias_label. + + https://www.braze.com/docs/api/endpoints/user_data/post_user_identify/ + The maximum email/user_id dictionary limit is 50, any length beyond 50 will raise an error. + + The trigger properties default to None and return as an empty dictionary if no individualized + trigger property is set based on the email. + + Arguments: + - `alias_label` (str): The alias label of the user + - `user_id_by_email` (dict): A dictionary where the key is the user's email (str) + and the value is the `lms_user_id` (int). + - `trigger_properties_by_email` (dict) : A dictionary where the key is the user's email (str) + and the value are the `trigger_properties` (dict) + Default is None + + Raises: + - `BrazeClientError`: if the number of entries in `user_id_by_email` exceeds 50. + + Returns: + - Dict: A dictionary where the key is the `user_email` (str) and the value is the metadata + relating to the braze recipient. + + Example: create_recipients( + 'alias_label'='Enterprise', + 'user_id_by_email'= { + 'hamzah@example.com': 123, + 'alex@example.com': 231, + }, + 'trigger_properties_by_email'= { + 'hamzah@example.com': { + 'foo':'bar' + }, + 'alex@example.com': {} + }, + ) + """ + if len(user_id_by_email) > MAX_NUM_IDENTIFY_USERS_ALIASES: + msg = "Max recipient limit reached." + raise BrazeClientError(msg) + + if trigger_properties_by_email is None: + trigger_properties_by_email = {} + + user_aliases_by_email = { + email: { + "alias_label": alias_label, + "alias_name": email, + } + for email in user_id_by_email + } + # Identify the user alias in case it already exists. This is necessary so + # we don't accidently create a duplicate Braze profile. + self.identify_users([ + { + 'external_id': lms_user_id, + 'user_alias': user_aliases_by_email.get(email) + } + for email, lms_user_id in user_id_by_email.items() + ]) + + attributes_by_email = { + email: { + "user_alias": user_aliases_by_email.get(email), + "email": email, + "is_enterprise_learner": True, + "_update_existing_only": False, + } + for email in user_id_by_email + } + + return { + email: { + 'external_user_id': lms_user_id, + 'attributes': attributes_by_email.get(email), + # If a profile does not already exist, Braze will create a new profile before sending a message. + 'send_to_existing_only': False, + 'trigger_properties': trigger_properties_by_email.get(email, {}), + } + for email, lms_user_id in user_id_by_email.items() + } + def track_user( self, attributes=None, diff --git a/braze/constants.py b/braze/constants.py index 0c2dcad..1c69641 100644 --- a/braze/constants.py +++ b/braze/constants.py @@ -28,6 +28,9 @@ class BrazeAPIEndpoints: # https://www.braze.com/docs/api/endpoints/export/user_data/post_users_identifier/?tab=all%20fields GET_EXTERNAL_IDS_CHUNK_SIZE = 50 +# https://www.braze.com/docs/api/endpoints/user_data/post_user_identify/ +MAX_NUM_IDENTIFY_USERS_ALIASES = 50 + UNSUBSCRIBED_STATE = 'unsubscribed' UNSUBSCRIBED_EMAILS_API_LIMIT = 500 UNSUBSCRIBED_EMAILS_API_SORT_DIRECTION = 'desc' diff --git a/test_utils/utils.py b/test_utils/utils.py new file mode 100644 index 0000000..aa2f89e --- /dev/null +++ b/test_utils/utils.py @@ -0,0 +1,18 @@ +""" +Utility functions for tests +""" +import math +import random +import string + + +def generate_emails_and_ids(num_emails): + """ + Generates random emails with random uuids used primarily to test length constraints + """ + emails_and_ids = { + ''.join(random.choices(string.ascii_uppercase + + string.digits, k=8)) + '@gmail.com': math.floor(random.random() * 1000) + for _ in range(num_emails) + } + return emails_and_ids diff --git a/tests/braze/test_client.py b/tests/braze/test_client.py index 644e73d..180072c 100644 --- a/tests/braze/test_client.py +++ b/tests/braze/test_client.py @@ -11,6 +11,7 @@ from braze.client import BrazeClient from braze.constants import ( GET_EXTERNAL_IDS_CHUNK_SIZE, + MAX_NUM_IDENTIFY_USERS_ALIASES, UNSUBSCRIBED_EMAILS_API_LIMIT, UNSUBSCRIBED_EMAILS_API_SORT_DIRECTION, BrazeAPIEndpoints, @@ -24,6 +25,7 @@ BrazeRateLimitError, BrazeUnauthorizedError, ) +from test_utils.utils import generate_emails_and_ids @ddt.ddt @@ -142,6 +144,96 @@ def test_identify_users(self): assert responses.calls[0].request.url == self.USERS_IDENTIFY_URL assert responses.calls[0].request.body == json.dumps(expected_body) + @responses.activate + def test_create_recipients_happy_path(self): + """ + Tests create recipients with multiple user emails + """ + responses.add( + responses.POST, + self.USERS_IDENTIFY_URL, + json={'message': 'success'}, + status=201 + ) + + mock_user_id_by_email = { + "test_email_1@example.com": 12345, + "test_email_2@example.com": 56789, + } + mock_trigger_properties_by_email = { + "test_email_1@example.com": { + 'test_property_name': True + }, + "test_email_3@example.com": { + 'test_property_address': True + }, + } + mock_expected_recipients = { + email: { + 'external_user_id': lms_user_id, + 'attributes': { + 'user_alias': { + 'alias_label': 'Enterprise', + 'alias_name': email + }, + 'email': email, + 'is_enterprise_learner': True, + '_update_existing_only': False, + }, + 'send_to_existing_only': False, + 'trigger_properties': mock_trigger_properties_by_email.get(email, {}) + + } + for email, lms_user_id in mock_user_id_by_email.items() + } + recipients = self.client.create_recipients( + alias_label='Enterprise', + user_id_by_email=mock_user_id_by_email, + trigger_properties_by_email=mock_trigger_properties_by_email, + ) + + assert len(recipients) == 2 + assert recipients == mock_expected_recipients + + def test_create_recipients_exceed_max_emails(self): + """ + Tests the maximum number of emails allowed per identify_users call + used within this function. + """ + mock_exceed_email_length = generate_emails_and_ids(MAX_NUM_IDENTIFY_USERS_ALIASES + 10) + try: + self.client.create_recipients( + alias_label='Enterprise', + user_id_by_email=mock_exceed_email_length, + ) + except BrazeClientError as error: + assert str(error) == "Max recipient limit reached." + + @responses.activate + def test_create_recipients_none_type_trigger_properties(self): + """ + Tests that when trigger_properties_by_email is not a defined parameter, + its output is transformed into an empty dictionary. + """ + responses.add( + responses.POST, + self.USERS_IDENTIFY_URL, + json={'message': 'success'}, + status=201 + ) + mock_user_id_by_email = { + "test_email_1@example.com": 12345, + "test_email_2@example.com": 56789, + } + + recipients = self.client.create_recipients( + alias_label='Enterprise', + user_id_by_email=mock_user_id_by_email, + ) + + for _, metadata in recipients.items(): + assert metadata.get('trigger_properties') == {} + def test_track_user_bad_args(self): """ Tests that arguments are validated.