Skip to content

Commit fca08e6

Browse files
authored
feat: adds 'create_recipients' function to the braze client (#25)
1 parent d6c7ac7 commit fca08e6

File tree

5 files changed

+208
-0
lines changed

5 files changed

+208
-0
lines changed

CHANGELOG.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,10 @@ Change Log
1414
Unreleased
1515
~~~~~~~~~~
1616

17+
[0.2.4]
18+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
19+
feat: adds 'create_recipients' function to the braze client
20+
1721
[0.2.3]
1822
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1923
feat: pass error response content into raised exceptions

braze/client.py

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
from braze.constants import (
1313
GET_EXTERNAL_IDS_CHUNK_SIZE,
14+
MAX_NUM_IDENTIFY_USERS_ALIASES,
1415
REQUEST_TYPE_GET,
1516
REQUEST_TYPE_POST,
1617
TRACK_USER_COMPONENT_CHUNK_SIZE,
@@ -209,6 +210,96 @@ def identify_users(self, aliases_to_identify):
209210

210211
return self._make_request(payload, BrazeAPIEndpoints.IDENTIFY_USERS, REQUEST_TYPE_POST)
211212

213+
def create_recipients(self, alias_label, user_id_by_email, trigger_properties_by_email=None):
214+
"""
215+
Create a recipient object using the dictionary, `user_id_by_email`
216+
containing the user_email key and `lms_user_id` value.
217+
Identifies a list of given email addresess with any existing Braze alias records
218+
via the provided ``lms_user_id``.
219+
220+
https://www.braze.com/docs/api/objects_filters/user_alias_object
221+
The user_alias objects requires a passed in alias_label.
222+
223+
https://www.braze.com/docs/api/endpoints/user_data/post_user_identify/
224+
The maximum email/user_id dictionary limit is 50, any length beyond 50 will raise an error.
225+
226+
The trigger properties default to None and return as an empty dictionary if no individualized
227+
trigger property is set based on the email.
228+
229+
Arguments:
230+
- `alias_label` (str): The alias label of the user
231+
- `user_id_by_email` (dict): A dictionary where the key is the user's email (str)
232+
and the value is the `lms_user_id` (int).
233+
- `trigger_properties_by_email` (dict) : A dictionary where the key is the user's email (str)
234+
and the value are the `trigger_properties` (dict)
235+
Default is None
236+
237+
Raises:
238+
- `BrazeClientError`: if the number of entries in `user_id_by_email` exceeds 50.
239+
240+
Returns:
241+
- Dict: A dictionary where the key is the `user_email` (str) and the value is the metadata
242+
relating to the braze recipient.
243+
244+
Example: create_recipients(
245+
'alias_label'='Enterprise',
246+
'user_id_by_email'= {
247+
'hamzah@example.com': 123,
248+
'alex@example.com': 231,
249+
},
250+
'trigger_properties_by_email'= {
251+
'hamzah@example.com': {
252+
'foo':'bar'
253+
},
254+
'alex@example.com': {}
255+
},
256+
)
257+
"""
258+
if len(user_id_by_email) > MAX_NUM_IDENTIFY_USERS_ALIASES:
259+
msg = "Max recipient limit reached."
260+
raise BrazeClientError(msg)
261+
262+
if trigger_properties_by_email is None:
263+
trigger_properties_by_email = {}
264+
265+
user_aliases_by_email = {
266+
email: {
267+
"alias_label": alias_label,
268+
"alias_name": email,
269+
}
270+
for email in user_id_by_email
271+
}
272+
# Identify the user alias in case it already exists. This is necessary so
273+
# we don't accidently create a duplicate Braze profile.
274+
self.identify_users([
275+
{
276+
'external_id': lms_user_id,
277+
'user_alias': user_aliases_by_email.get(email)
278+
}
279+
for email, lms_user_id in user_id_by_email.items()
280+
])
281+
282+
attributes_by_email = {
283+
email: {
284+
"user_alias": user_aliases_by_email.get(email),
285+
"email": email,
286+
"is_enterprise_learner": True,
287+
"_update_existing_only": False,
288+
}
289+
for email in user_id_by_email
290+
}
291+
292+
return {
293+
email: {
294+
'external_user_id': lms_user_id,
295+
'attributes': attributes_by_email.get(email),
296+
# If a profile does not already exist, Braze will create a new profile before sending a message.
297+
'send_to_existing_only': False,
298+
'trigger_properties': trigger_properties_by_email.get(email, {}),
299+
}
300+
for email, lms_user_id in user_id_by_email.items()
301+
}
302+
212303
def track_user(
213304
self,
214305
attributes=None,

braze/constants.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,9 @@ class BrazeAPIEndpoints:
2828
# https://www.braze.com/docs/api/endpoints/export/user_data/post_users_identifier/?tab=all%20fields
2929
GET_EXTERNAL_IDS_CHUNK_SIZE = 50
3030

31+
# https://www.braze.com/docs/api/endpoints/user_data/post_user_identify/
32+
MAX_NUM_IDENTIFY_USERS_ALIASES = 50
33+
3134
UNSUBSCRIBED_STATE = 'unsubscribed'
3235
UNSUBSCRIBED_EMAILS_API_LIMIT = 500
3336
UNSUBSCRIBED_EMAILS_API_SORT_DIRECTION = 'desc'

test_utils/utils.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
"""
2+
Utility functions for tests
3+
"""
4+
import math
5+
import random
6+
import string
7+
8+
9+
def generate_emails_and_ids(num_emails):
10+
"""
11+
Generates random emails with random uuids used primarily to test length constraints
12+
"""
13+
emails_and_ids = {
14+
''.join(random.choices(string.ascii_uppercase +
15+
string.digits, k=8)) + '@gmail.com': math.floor(random.random() * 1000)
16+
for _ in range(num_emails)
17+
}
18+
return emails_and_ids

tests/braze/test_client.py

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from braze.client import BrazeClient
1212
from braze.constants import (
1313
GET_EXTERNAL_IDS_CHUNK_SIZE,
14+
MAX_NUM_IDENTIFY_USERS_ALIASES,
1415
UNSUBSCRIBED_EMAILS_API_LIMIT,
1516
UNSUBSCRIBED_EMAILS_API_SORT_DIRECTION,
1617
BrazeAPIEndpoints,
@@ -24,6 +25,7 @@
2425
BrazeRateLimitError,
2526
BrazeUnauthorizedError,
2627
)
28+
from test_utils.utils import generate_emails_and_ids
2729

2830

2931
@ddt.ddt
@@ -142,6 +144,96 @@ def test_identify_users(self):
142144
assert responses.calls[0].request.url == self.USERS_IDENTIFY_URL
143145
assert responses.calls[0].request.body == json.dumps(expected_body)
144146

147+
@responses.activate
148+
def test_create_recipients_happy_path(self):
149+
"""
150+
Tests create recipients with multiple user emails
151+
"""
152+
responses.add(
153+
responses.POST,
154+
self.USERS_IDENTIFY_URL,
155+
json={'message': 'success'},
156+
status=201
157+
)
158+
159+
mock_user_id_by_email = {
160+
"test_email_1@example.com": 12345,
161+
"test_email_2@example.com": 56789,
162+
}
163+
mock_trigger_properties_by_email = {
164+
"test_email_1@example.com": {
165+
'test_property_name': True
166+
},
167+
"test_email_3@example.com": {
168+
'test_property_address': True
169+
},
170+
}
171+
mock_expected_recipients = {
172+
email: {
173+
'external_user_id': lms_user_id,
174+
'attributes': {
175+
'user_alias': {
176+
'alias_label': 'Enterprise',
177+
'alias_name': email
178+
},
179+
'email': email,
180+
'is_enterprise_learner': True,
181+
'_update_existing_only': False,
182+
},
183+
'send_to_existing_only': False,
184+
'trigger_properties': mock_trigger_properties_by_email.get(email, {})
185+
186+
}
187+
for email, lms_user_id in mock_user_id_by_email.items()
188+
}
189+
recipients = self.client.create_recipients(
190+
alias_label='Enterprise',
191+
user_id_by_email=mock_user_id_by_email,
192+
trigger_properties_by_email=mock_trigger_properties_by_email,
193+
)
194+
195+
assert len(recipients) == 2
196+
assert recipients == mock_expected_recipients
197+
198+
def test_create_recipients_exceed_max_emails(self):
199+
"""
200+
Tests the maximum number of emails allowed per identify_users call
201+
used within this function.
202+
"""
203+
mock_exceed_email_length = generate_emails_and_ids(MAX_NUM_IDENTIFY_USERS_ALIASES + 10)
204+
try:
205+
self.client.create_recipients(
206+
alias_label='Enterprise',
207+
user_id_by_email=mock_exceed_email_length,
208+
)
209+
except BrazeClientError as error:
210+
assert str(error) == "Max recipient limit reached."
211+
212+
@responses.activate
213+
def test_create_recipients_none_type_trigger_properties(self):
214+
"""
215+
Tests that when trigger_properties_by_email is not a defined parameter,
216+
its output is transformed into an empty dictionary.
217+
"""
218+
responses.add(
219+
responses.POST,
220+
self.USERS_IDENTIFY_URL,
221+
json={'message': 'success'},
222+
status=201
223+
)
224+
mock_user_id_by_email = {
225+
"test_email_1@example.com": 12345,
226+
"test_email_2@example.com": 56789,
227+
}
228+
229+
recipients = self.client.create_recipients(
230+
alias_label='Enterprise',
231+
user_id_by_email=mock_user_id_by_email,
232+
)
233+
234+
for _, metadata in recipients.items():
235+
assert metadata.get('trigger_properties') == {}
236+
145237
def test_track_user_bad_args(self):
146238
"""
147239
Tests that arguments are validated.

0 commit comments

Comments
 (0)