Skip to content

Commit 576b309

Browse files
authored
[TOOLSLIBS-446 et al] Named user updates (#179)
* adds paylaods props and DRYs out * adds property tests * fix formatting and docstring * adds named user uninstall * adds named user update * adds named user attributes
1 parent b005c16 commit 576b309

File tree

3 files changed

+261
-34
lines changed

3 files changed

+261
-34
lines changed

tests/devices/test_named_user.py

Lines changed: 55 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -43,12 +43,11 @@ def test_Named_User(self):
4343

4444
nu = ua.NamedUser(airship, "name1")
4545

46-
associate = nu.associate("channel_id", "ios")
47-
46+
associate = nu.associate(channel_id="channel_id", device_type="ios")
4847
self.assertEqual(associate.status_code, 200)
4948
self.assertEqual(associate.ok, True)
5049

51-
disassociate = nu.disassociate("channel_id", "ios")
50+
disassociate = nu.disassociate(channel_id="channel_id", device_type="ios")
5251
self.assertEqual(disassociate.status_code, 200)
5352
self.assertEqual(disassociate.ok, True)
5453

@@ -60,6 +59,43 @@ def test_Named_User(self):
6059
{"named_user_id": "name1", "tags": {"group_name": ["tag1", "tag2"]}},
6160
)
6261

62+
def test_channel_associate_payload_property(self):
63+
named_user = ua.NamedUser(
64+
airship=ua.Airship(TEST_KEY, TEST_SECRET), named_user_id="cowboy_dan"
65+
)
66+
named_user.channel_id = "524bb82-8499-4ba5-b313-2157b1b1771f"
67+
named_user.device_type = "ios"
68+
69+
self.assertEqual(
70+
named_user._channel_associate_payload,
71+
{
72+
"named_user_id": "cowboy_dan",
73+
"channel_id": "524bb82-8499-4ba5-b313-2157b1b1771f",
74+
"device_type": "ios",
75+
},
76+
)
77+
78+
def test_email_associate_payload_property(self):
79+
named_user = ua.NamedUser(
80+
airship=ua.Airship(TEST_KEY, TEST_SECRET), named_user_id="cowboy_dan"
81+
)
82+
named_user.email_address = "[email protected]"
83+
84+
self.assertEqual(
85+
named_user._email_associate_payload,
86+
{
87+
"named_user_id": "cowboy_dan",
88+
"email_address": "[email protected]",
89+
},
90+
)
91+
92+
def test_named_user_uninstall_raises(self):
93+
with self.assertRaises(ValueError):
94+
ua.NamedUser.uninstall(
95+
airship=ua.Airship(TEST_KEY, TEST_SECRET),
96+
named_users="should_be_a_list",
97+
)
98+
6399
def test_named_user_tag(self):
64100
airship = ua.Airship(TEST_KEY, TEST_SECRET)
65101
nu = ua.NamedUser(airship, "named_user_id")
@@ -72,6 +108,22 @@ def test_named_user_tag(self):
72108
set={"group": "other_tag"},
73109
)
74110

111+
def test_named_user_update_raises(self):
112+
airship = ua.Airship(TEST_KEY, TEST_SECRET)
113+
nu = ua.NamedUser(airship, "named_user_id")
114+
115+
with self.assertRaises(ValueError):
116+
nu.update()
117+
118+
def test_named_user_attributes_raises(self):
119+
airship = ua.Airship(TEST_KEY, TEST_SECRET)
120+
nu = ua.NamedUser(airship, "named_user_id")
121+
122+
with self.assertRaises(ValueError):
123+
nu.attributes(
124+
attributes={"action": "set", "key": "type", "value": "not_a_list"}
125+
)
126+
75127

76128
class TestNamedUserList(unittest.TestCase):
77129
def test_NamedUserlist_iteration(self):

urbanairship/core.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ def __init__(self, location=None):
4343
self.named_user_tag_url = self.named_user_url + "tags/"
4444
self.named_user_disassociate_url = self.named_user_url + "disassociate/"
4545
self.named_user_associate_url = self.named_user_url + "associate/"
46+
self.named_user_uninstall_url = self.named_user_url + "uninstall/"
4647

4748
self.sms_url = self.channel_url + "sms/"
4849
self.sms_opt_out_url = self.sms_url + "opt-out/"

urbanairship/devices/named_users.py

Lines changed: 205 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
from urbanairship.devices import ChannelTags
55
from urbanairship import common
6+
from urbanairship.push.payload import device_types
67

78
logger = logging.getLogger("urbanairship")
89

@@ -14,57 +15,123 @@ def __init__(self, airship, named_user_id=None):
1415

1516
self._airship = airship
1617
self.named_user_id = named_user_id
18+
self.channel_id = None
19+
self.email_address = None
20+
self.device_type = None
1721

18-
def associate(self, channel_id, device_type):
19-
"""Associate a channel with a named user ID
22+
@property
23+
def _channel_associate_payload(self):
24+
"""
25+
creates the paylaod for channel_id associate and disassociate calls
26+
"""
27+
payload = {"named_user_id": self.named_user_id, "channel_id": self.channel_id}
28+
29+
if self.device_type:
30+
payload["device_type"] = self.device_type
31+
32+
return payload
33+
34+
@property
35+
def _email_associate_payload(self):
36+
"""
37+
creates the payload for email_address associate and disassociate calls
38+
"""
39+
return {
40+
"named_user_id": self.named_user_id,
41+
"email_address": self.email_address,
42+
}
43+
44+
def _dis_associate(self, url, body):
45+
response = self._airship.request(
46+
method="POST",
47+
body=json.dumps(body),
48+
url=url,
49+
content_type="application/json",
50+
version=3,
51+
)
52+
53+
return response
54+
55+
def associate(self, channel_id, device_type=None):
56+
"""Associate a channel_id with a named user ID.
57+
Either channel_id and device_type OR email_address must be included.
2058
21-
:param channel_id: The ID of the channel you would like to associate
59+
:param channel_id: Required. The ID of the channel you would like to associate
2260
with the named user
23-
:param device_type: The device type of the channel
61+
:param device_type: The device type of the channel, do not include for web
62+
notify channel_ids
63+
2464
:return:
2565
"""
2666
if not self.named_user_id:
2767
raise ValueError("named_user_id is required for association")
2868

29-
body = json.dumps(
30-
{
31-
"channel_id": channel_id,
32-
"device_type": device_type,
33-
"named_user_id": self.named_user_id,
34-
}
35-
).encode("utf-8")
36-
response = self._airship._request(
37-
"POST",
38-
body,
39-
self._airship.urls.get("named_user_associate_url"),
40-
"application/json",
41-
version=3,
69+
self.channel_id = channel_id
70+
71+
if device_type:
72+
self.device_type = device_type
73+
74+
return self._dis_associate(
75+
url=self._airship.urls.get("named_user_associate_url"),
76+
body=self._channel_associate_payload,
4277
)
43-
return response
4478

45-
def disassociate(self, channel_id, device_type):
79+
def email_associate(self, email_address):
80+
"""Associate an email_address with a named user id. This call is for a literal
81+
email address.
82+
83+
:param email_address: Required. The email address to associate.
84+
85+
:return:
86+
"""
87+
if not self.named_user_id:
88+
raise ValueError("named_user_id is required for association")
89+
90+
self.email_address = email_address
91+
92+
return self._dis_associate(
93+
url=self._airship.urls.get("named_user_associate_url"),
94+
body=self._email_associate_payload,
95+
)
96+
97+
def disassociate(self, channel_id, device_type=None):
4698
"""Disassociate a channel with a named user ID
4799
48100
:param channel_id: The ID of the channel you would like to disassociate
49-
:param device_type: The device type of the channel
101+
:param device_type: The device type of the channel. Do not include for web
102+
notify channels.
103+
50104
:return:
51105
"""
106+
if not self.named_user_id:
107+
raise ValueError("named_user_id is required for association")
52108

53-
payload = {"channel_id": channel_id, "device_type": device_type}
109+
self.channel_id = channel_id
54110

55-
if self.named_user_id:
56-
payload["named_user_id"] = self.named_user_id
111+
if device_type:
112+
self.device_type = device_type
57113

58-
body = json.dumps(payload).encode("utf-8")
59-
response = self._airship._request(
60-
"POST",
61-
body,
62-
self._airship.urls.get("named_user_disassociate_url"),
63-
"application/json",
64-
version=3,
114+
return self._dis_associate(
115+
url=self._airship.urls.get("named_user_disassociate_url"),
116+
body=self._channel_associate_payload,
65117
)
66118

67-
return response
119+
def email_disassociate(self, email_address):
120+
"""Disassociate an email_address with a named user ID
121+
122+
:param channel_id: The email_address you would like to disassociate
123+
124+
:return:
125+
"""
126+
if not self.named_user_id:
127+
raise ValueError("named_user_id is required for association")
128+
129+
self.email_address = email_address
130+
131+
return self._dis_associate(
132+
url=self._airship.urls.get("named_user_disassociate_url"),
133+
body=self._email_associate_payload,
134+
)
68135

69136
def lookup(self):
70137
"""Lookup a single named user
@@ -125,6 +192,113 @@ def tag(self, group, add=None, remove=None, set=None):
125192

126193
return response.json()
127194

195+
def update(self, associate=None, disassociate=None, tags=None, attributes=None):
196+
"""
197+
Create or update a named user by associating/disassociating channels
198+
and adding/removing tags and attributes in a single request.
199+
If a channel has an assigned named user and you make an additional call to
200+
associate that same channel with a new named user, the original named user
201+
association will be removed and the new named user and associated data will
202+
take its place. Additionally, all tags associated to the original named
203+
user cannot be used to address this channel unless they are also associated
204+
with the new named user.
205+
206+
Please see https://docs.airship.com/api/ua/#operation-api-named_users-named_user_id-post
207+
208+
:param assocaite: Optional. List of objects to associate with the named user
209+
:param disassociate: Optional. List of objects to disassociate from the named
210+
user
211+
:param tags: Optional. Dictionary of set, add, remove objects to apply to
212+
named user
213+
:param attributes: Optional. List of attributes to apply to named user.
214+
215+
:return:
216+
"""
217+
if not any([associate, disassociate, tags, attributes]):
218+
raise ValueError(
219+
"At least one of associate, disassociate, tags, or attributes must be included"
220+
)
221+
222+
body = {}
223+
224+
if associate:
225+
body["associate"] = associate
226+
if disassociate:
227+
body["disassociate"] = disassociate
228+
if tags:
229+
body["tags"] = tags
230+
if attributes:
231+
body["attributes"] = attributes
232+
233+
response = self._airship.request(
234+
method="POST",
235+
body=json.dumps(body),
236+
url=self._airship.urls.get("named_user_url") + self.named_user_id,
237+
content_type="application/json",
238+
version=3,
239+
)
240+
241+
return response
242+
243+
def attributes(self, attributes):
244+
"""
245+
Set or remove attributes on a named user.
246+
A single request body may contain a set or remove field, or both, or a single
247+
set field. If both set and remove fields are present and the intersection of
248+
the attributes in these fields is not empty, then a 400 will be returned.
249+
If an attribute request is partially valid, i.e. at least one attribute exists,
250+
Airship returns a 200 with a warning in containing a list of attributes that
251+
failed to update.
252+
Please see https://docs.airship.com/api/ua/#operation-api-named_users-named_user_id-attributes-post
253+
for more about using Airship attributes.
254+
255+
:params attributes: Required. A list of attribute objects to set or remove on
256+
the named user.
257+
"""
258+
if type(attributes) is not list:
259+
raise ValueError("attributes must be a list of attribute objects")
260+
261+
response = self._airship.request(
262+
method="POST",
263+
body=json.dumps({"attributes": attributes}),
264+
url=self._airship.urls.get("named_user_url")
265+
+ self.named_user_id
266+
+ "attributes",
267+
content_type="application/json",
268+
version=3,
269+
)
270+
271+
return response
272+
273+
@classmethod
274+
def uninstall(cls, airship, named_users):
275+
"""
276+
Disassociate and delete all channels associated with the named_user_id(s) and
277+
also delete the named_user_id(s). This call removes all channels associated
278+
with a named user from Airship systems in compliance with data privacy laws.
279+
Uninstalling channels also removes accompanying analytic data (including
280+
Performance Analytics) from the system.
281+
Channel uninstallation, like channel creation, is an asynchronous operation,
282+
and may take some time to complete.
283+
284+
:param airship: Required. An urbanairship.Airship instance.
285+
:param named_users: Required a list of named_user_ids to completely uninstall
286+
287+
:return:
288+
"""
289+
if type(named_users) is not list:
290+
raise ValueError("named_users must be a list")
291+
292+
response = airship.request(
293+
method="POST",
294+
body=json.dumps({"named_user_id": named_users}),
295+
url=airship.urls.get("named_user_uninstall_url"),
296+
content_type="application/json",
297+
version=3,
298+
)
299+
300+
return response
301+
128302
@classmethod
129303
def from_payload(cls, payload):
130304
"""

0 commit comments

Comments
 (0)