Skip to content
This repository was archived by the owner on Sep 6, 2025. It is now read-only.

Commit ddf2b69

Browse files
Merge pull request #16 from hellofresh/feature/implement-async-user-management
Add support to Crossengage API v2
2 parents 46d94e0 + 73c7771 commit ddf2b69

File tree

4 files changed

+310
-87
lines changed

4 files changed

+310
-87
lines changed

crossengage/client.py

Lines changed: 120 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import requests
55
from requests.exceptions import RequestException
66

7+
from crossengage.utils import update_dict
78

89
class CrossengageClient(object):
910
"""
@@ -39,11 +40,18 @@ class CrossengageClient(object):
3940
4041
"""
4142
API_URL = 'https://api.crossengage.io'
42-
API_VERSION = '1'
43+
API_VERSIONS = {
44+
"v1": "1",
45+
"v2": "2"
46+
}
4347

44-
USER_ENDPOINT = '/users/'
45-
EVENTS_ENDPOINT = '/events'
46-
BULK_ENDPOINT = '/users/batch'
48+
AUTH_HEADER = 'X-XNG-AuthToken'
49+
API_VERSION_HEADER = 'X-XNG-ApiVersion'
50+
51+
USER_ENDPOINT = 'users'
52+
USER_BULK_ENDPOINT = "{0}/batch".format(USER_ENDPOINT)
53+
TRACK_USER_TASK_ENDPOINT = "{0}/track".format(USER_ENDPOINT)
54+
EVENTS_ENDPOINT = 'events'
4755

4856
REQUEST_GET = 'get'
4957
REQUEST_PUT = 'put'
@@ -62,12 +70,33 @@ def __init__(self, client_token):
6270
self.client_token = client_token
6371
self.requests = requests
6472
self.request_url = ''
65-
self.headers = {
66-
'X-XNG-AuthToken': self.client_token,
67-
'X-XNG-ApiVersion': self.API_VERSION,
73+
self.default_headers = {
74+
self.AUTH_HEADER: self.client_token,
75+
self.API_VERSION_HEADER: self.API_VERSIONS["v1"],
6876
'Content-Type': 'application/json',
6977
}
7078

79+
def get_user(self, user):
80+
# type: (dict) -> dict
81+
"""
82+
Fetch User by id.
83+
:param user: dict of payload (id, email, businessUnit, firstName, lastName, birthday, createdAt, gender)
84+
:return: json dict response, for example:
85+
{
86+
"status_code": 200,
87+
"email": "john.doe@crossengage.io",
88+
"id": "fb85fe50-a528-11e7-abc4-cec278b6b50a",
89+
"xngId": "123e4567-e89b-12d3-a456-426655440000",
90+
"firstName": "John",
91+
"lastName": "Doe",
92+
"birthday": "1982-08-30",
93+
"createdAt": "2015-10-02T08:23:53Z",
94+
"gender": "male"
95+
}
96+
"""
97+
self.request_url = "{0}/{1}/{2}".format(self.API_URL, self.USER_ENDPOINT, user['id'])
98+
return self.__create_request(payload={}, request_type=self.REQUEST_GET, version="v2")
99+
71100
def update_user(self, user):
72101
# type: (dict) -> dict
73102
"""
@@ -76,8 +105,19 @@ def update_user(self, user):
76105
:return: json dict response, for example: {"status_code": 200, "id":"123", "xngGlobalUserId": "xng-id",
77106
"success": "true}
78107
"""
79-
self.request_url = self.API_URL + self.USER_ENDPOINT + user['id']
80-
return self.__create_request(payload=user, request_type=self.REQUEST_PUT)
108+
self.request_url = "{0}/{1}/{2}".format(self.API_URL, self.USER_ENDPOINT, user['id'])
109+
return self.__create_request(payload=user, request_type=self.REQUEST_PUT, version="v1")
110+
111+
def update_user_async(self, user):
112+
# type: (dict) -> dict
113+
"""
114+
Create / Update User given its id and email.
115+
:param user: dict of payload (id, email, businessUnit, firstName, lastName, birthday, createdAt, gender)
116+
:return: json dict response, for example:
117+
{"status_code": 202, "trackingId": "2e312089-a987-45c6-adbd-b904bc4dfc97"}
118+
"""
119+
self.request_url = "{0}/{1}".format(self.API_URL, self.USER_ENDPOINT)
120+
return self.__create_request(payload=user, request_type=self.REQUEST_PUT, version="v2")
81121

82122
def update_users_bulk(self, users):
83123
# type: (list) -> dict
@@ -88,8 +128,8 @@ def update_users_bulk(self, users):
88128
:return: json dict response
89129
"""
90130
payload = {'updated': users}
91-
self.request_url = self.API_URL + self.USER_ENDPOINT + 'batch'
92-
return self.__create_request(payload=payload, request_type=self.REQUEST_POST)
131+
self.request_url = "{0}/{1}".format(self.API_URL, self.USER_BULK_ENDPOINT)
132+
return self.__create_request(payload=payload, request_type=self.REQUEST_POST, version="v1")
93133

94134
def delete_user(self, user):
95135
# type: (dict) -> dict
@@ -98,8 +138,19 @@ def delete_user(self, user):
98138
:param user: dict of payload (id)
99139
:return: json dict response, for example: {"status_code": 200}
100140
"""
101-
self.request_url = self.API_URL + self.USER_ENDPOINT + user['id']
102-
return self.__create_request(payload=user, request_type=self.REQUEST_DELETE)
141+
self.request_url = "{0}/{1}/{2}".format(self.API_URL, self.USER_ENDPOINT, user['id'])
142+
return self.__create_request(payload=user, request_type=self.REQUEST_DELETE, version="v1")
143+
144+
def delete_user_async(self, user):
145+
# type: (dict) -> dict
146+
"""
147+
Delete User given its id.
148+
:param user: dict of payload (id)
149+
:return: json dict response, for example:
150+
{"status_code": 202, "trackingId": "2e312089-a987-45c6-adbd-b904bc4dfc97"}
151+
"""
152+
self.request_url = "{0}/{1}/{2}".format(self.API_URL, self.USER_ENDPOINT, user['id'])
153+
return self.__create_request(payload=user, request_type=self.REQUEST_DELETE, version="v2")
103154

104155
def delete_user_by_xng_id(self, user):
105156
# type: (dict) -> dict
@@ -108,8 +159,8 @@ def delete_user_by_xng_id(self, user):
108159
:param user: dict of payload (xng_id)
109160
:return: json dict response, for example: {"status_code": 200}
110161
"""
111-
self.request_url = self.API_URL + self.USER_ENDPOINT + 'xngId/' + user['xngId']
112-
return self.__create_request(payload=user, request_type=self.REQUEST_DELETE)
162+
self.request_url = "{0}/{1}/xngId/{2}".format(self.API_URL, self.USER_ENDPOINT, user['xngId'])
163+
return self.__create_request(payload=user, request_type=self.REQUEST_DELETE, version="v1")
113164

114165
def add_user_attribute(self, attribute_name, attribute_type, nested_type):
115166
"""
@@ -120,13 +171,13 @@ def add_user_attribute(self, attribute_name, attribute_type, nested_type):
120171
:return: json dict response, for example: {"id": 123, "name":"traits.foobar", "attributeType": "ARRAY",
121172
"success": "true}
122173
"""
123-
self.request_url = self.API_URL + self.USER_ENDPOINT + 'attributes'
174+
self.request_url = "{0}/{1}/attributes".format(self.API_URL, self.USER_ENDPOINT)
124175
payload = {
125176
'name': 'traits.' + attribute_name,
126177
'attributeType': attribute_type,
127178
'nestedType': nested_type
128179
}
129-
return self.__create_request(payload, self.REQUEST_POST)
180+
return self.__create_request(payload, self.REQUEST_POST, version="v1")
130181

131182
def add_nested_user_attribute(self, parent_name, attribute_name, attribute_type):
132183
"""
@@ -137,13 +188,13 @@ def add_nested_user_attribute(self, parent_name, attribute_name, attribute_type)
137188
:return: json dict response, for example: {"id": 123, "name":"traits.foobar", "attributeType": "ARRAY",
138189
"success": "true}
139190
"""
140-
self.request_url = self.API_URL + self.USER_ENDPOINT + 'attributes'
191+
self.request_url = "{0}/{1}/attributes".format(self.API_URL, self.USER_ENDPOINT)
141192
payload = {
142193
'name': attribute_name,
143194
'attributeType': attribute_type,
144195
'parentName': parent_name
145196
}
146-
return self.__create_request(payload, self.REQUEST_POST)
197+
return self.__create_request(payload, self.REQUEST_POST, version="v1")
147198

148199
def list_user_attributes(self, offset, limit):
149200
"""
@@ -155,17 +206,19 @@ def list_user_attributes(self, offset, limit):
155206
"""
156207
self.request_url = self.API_URL + self.USER_ENDPOINT + 'attributes?offset=' + str(offset) + '&limit=' + str(
157208
limit)
158-
return self.__create_request(None, self.REQUEST_GET)
209+
self.request_url = "{0}/{1}/attributes?offset={2}&limit={3}".format(
210+
self.API_URL, self.USER_ENDPOINT, offset, limit)
211+
return self.__create_request(None, self.REQUEST_GET, version="v1")
159212

160213
def delete_user_attribute(self, attribute_id):
161214
"""
162215
Delete user attribute.
163216
:param attribute_id: id of attribute
164217
:return: response N/A or error_response
165218
"""
166-
self.request_url = self.API_URL + self.USER_ENDPOINT + 'attributes/' + str(attribute_id)
219+
self.request_url = "{0}/{1}/attributes/{2}".format(self.API_URL, self.USER_ENDPOINT, attribute_id)
167220
payload = {}
168-
return self.__create_request(payload, self.REQUEST_DELETE)
221+
return self.__create_request(payload, self.REQUEST_DELETE, version="v1")
169222

170223
def send_events(self, events, email=None, user_id=None, business_unit=None):
171224
"""
@@ -176,7 +229,7 @@ def send_events(self, events, email=None, user_id=None, business_unit=None):
176229
:param user_id: id of user in your database
177230
:return: json dict response, for example: {"status_code": 200}
178231
"""
179-
self.request_url = "{}{}".format(self.API_URL, self.EVENTS_ENDPOINT)
232+
self.request_url = "{0}/{1}".format(self.API_URL, self.EVENTS_ENDPOINT)
180233

181234
if email is None and user_id is None:
182235
raise ValueError('email or external_id required for sending events')
@@ -194,7 +247,7 @@ def send_events(self, events, email=None, user_id=None, business_unit=None):
194247
if business_unit is not None:
195248
payload['businessUnit'] = business_unit
196249

197-
return self.__create_request(payload, self.REQUEST_POST)
250+
return self.__create_request(payload, self.REQUEST_POST, version="v1")
198251

199252
def batch_process(self, delete_list=[], update_list=[]):
200253
"""
@@ -239,7 +292,7 @@ def batch_process(self, delete_list=[], update_list=[]):
239292
]
240293
}
241294
"""
242-
self.request_url = self.API_URL + self.BULK_ENDPOINT
295+
self.request_url = "{0}/{1}".format(self.API_URL, self.USER_BULK_ENDPOINT)
243296
payload = {
244297
'updated': update_list,
245298
'deleted': delete_list,
@@ -248,25 +301,60 @@ def batch_process(self, delete_list=[], update_list=[]):
248301
r = self.requests.post(
249302
self.request_url,
250303
data=json.dumps(payload),
251-
headers=self.headers
304+
headers=self.default_headers
252305
)
253306

254307
return r.status_code, r.json()
255308

256-
def __create_request(self, payload, request_type):
257-
r = '{}'
309+
def batch_process_async(self, delete_list=[], update_list=[]):
310+
"""
311+
Create, Update or Delete up to 1000 users in batch.
312+
:param delete_list: users that should be deleted
313+
:param update_list: users that should be created or updated
314+
:return integer status_code, json dict response
315+
202, {"trackingId": "2e312089-a987-45c6-adbd-b904bc4dfc97"}
316+
"""
317+
headers = update_dict(self.default_headers, {self.API_VERSION_HEADER: self.API_VERSIONS["v2"]})
318+
self.request_url = "{0}/{1}".format(self.API_URL, self.USER_BULK_ENDPOINT)
319+
320+
payload = {
321+
'updated': update_list,
322+
'deleted': delete_list,
323+
}
324+
325+
r = self.requests.post(self.request_url, data=json.dumps(payload), headers=headers)
326+
327+
return r.status_code, r.json()
328+
329+
def track_user_task(self, tracking_id):
330+
# type: (dict) -> dict
331+
"""
332+
Create / Update User given its id.
333+
:param user: dict of payload (email, id, firstName, lastName, birthday, createdAt, gender)
334+
:return integer status_code, json dict response
335+
200, { "stage": "PROCESSED", "total": 2, "success": 1, "error": 1 }
336+
"""
337+
headers = update_dict(self.default_headers, {self.API_VERSION_HEADER: self.API_VERSIONS["v2"]})
338+
self.request_url = "{0}/{1}/{2}".format(self.API_URL, self.TRACK_USER_TASK_ENDPOINT, tracking_id)
339+
340+
r = self.requests.get(self.request_url, headers=headers)
341+
342+
return r.status_code, r.json()
343+
344+
def __create_request(self, payload, request_type, version):
345+
headers = update_dict(self.default_headers, {self.API_VERSION_HEADER: self.API_VERSIONS[version]})
258346
try:
259347
if request_type == self.REQUEST_PUT:
260-
r = self.requests.put(self.request_url, data=json.dumps(payload), headers=self.headers)
348+
r = self.requests.put(self.request_url, data=json.dumps(payload), headers=headers)
261349

262350
if request_type == self.REQUEST_GET:
263-
r = self.requests.get(self.request_url, headers=self.headers)
351+
r = self.requests.get(self.request_url, headers=headers)
264352

265353
if request_type == self.REQUEST_POST:
266-
r = self.requests.post(self.request_url, data=json.dumps(payload), headers=self.headers)
354+
r = self.requests.post(self.request_url, data=json.dumps(payload), headers=headers)
267355

268356
if request_type == self.REQUEST_DELETE:
269-
r = self.requests.delete(self.request_url, data=json.dumps(payload), headers=self.headers)
357+
r = self.requests.delete(self.request_url, data=json.dumps(payload), headers=headers)
270358

271359
response = {}
272360
if r.text != '':

crossengage/utils.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
def update_dict(old_dict, values):
2+
""" Update dictionary without change the original object """
3+
new_dict = old_dict.copy()
4+
new_dict.update(values)
5+
return new_dict

0 commit comments

Comments
 (0)