Skip to content

Commit

Permalink
feat: adds 'create_recipients' function to the braze client (#25)
Browse files Browse the repository at this point in the history
  • Loading branch information
brobro10000 authored May 1, 2024
1 parent d6c7ac7 commit fca08e6
Show file tree
Hide file tree
Showing 5 changed files with 208 additions and 0 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
91 changes: 91 additions & 0 deletions braze/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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'= {
'[email protected]': 123,
'[email protected]': 231,
},
'trigger_properties_by_email'= {
'[email protected]': {
'foo':'bar'
},
'[email protected]': {}
},
)
"""
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,
Expand Down
3 changes: 3 additions & 0 deletions braze/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'
18 changes: 18 additions & 0 deletions test_utils/utils.py
Original file line number Diff line number Diff line change
@@ -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
92 changes: 92 additions & 0 deletions tests/braze/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -24,6 +25,7 @@
BrazeRateLimitError,
BrazeUnauthorizedError,
)
from test_utils.utils import generate_emails_and_ids


@ddt.ddt
Expand Down Expand Up @@ -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 = {
"[email protected]": 12345,
"[email protected]": 56789,
}
mock_trigger_properties_by_email = {
"[email protected]": {
'test_property_name': True
},
"[email protected]": {
'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 = {
"[email protected]": 12345,
"[email protected]": 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.
Expand Down

0 comments on commit fca08e6

Please sign in to comment.