Skip to content

Commit 1c0eee5

Browse files
authored
Merge pull request #60 from adobe-apiplatform/v2
prepare for version 2.9 release
2 parents a212241 + 1e96229 commit 1c0eee5

File tree

5 files changed

+231
-2
lines changed

5 files changed

+231
-2
lines changed

HISTORY.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,3 +119,10 @@ Because the UMAPI functionality around Adobe IDs is now different for migrated o
119119
* Allow setting attributes on Adobe ID users as long as the server allows it.
120120
* [Issue 55](https://github.com/adobe-apiplatform/umapi-client.py/issues/55)
121121
* Don't default the country code when creating new Enterprise ID users.
122+
123+
### Version 2.9
124+
125+
Bug fix release:
126+
127+
* [Issue 58](https://github.com/adobe-apiplatform/umapi-client.py/issues/58)
128+
* Error when adding more than 10 groups in a single action step.

tests/test_connections.py

Lines changed: 178 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,14 @@
2121
import time
2222
from email.utils import formatdate
2323

24+
import six
2425
import mock
2526
import pytest
2627
import requests
2728

2829
from conftest import mock_connection_params, MockResponse
29-
from umapi_client import Connection, UnavailableError, ServerError, RequestError
30+
from umapi_client import Connection, UnavailableError, ServerError, RequestError, UserAction, GroupTypes, \
31+
IdentityTypes, RoleTypes
3032
from umapi_client import __version__ as umapi_version
3133

3234

@@ -242,3 +244,178 @@ def test_post_request_fail():
242244
mock_post.return_value = MockResponse(400, text="400 test request failure")
243245
conn = Connection(**mock_connection_params)
244246
pytest.raises(RequestError, conn.make_call, "", "[3, 5]")
247+
248+
249+
def test_large_group_assignment_split():
250+
"""
251+
Ensure that large group list can be split into multiple commands
252+
:return:
253+
"""
254+
group_prefix = "G"
255+
add_groups = [group_prefix+six.text_type(n+1) for n in range(0, 15)]
256+
user = UserAction(id_type=IdentityTypes.enterpriseID, email="[email protected]")
257+
user.add_to_groups(groups=add_groups, group_type=GroupTypes.usergroup)
258+
assert user.maybe_split_groups(10) is True
259+
assert len(user.commands) == 2
260+
assert user.commands[0]["add"][GroupTypes.usergroup.name] == add_groups[0:10]
261+
assert user.commands[1]["add"][GroupTypes.usergroup.name] == add_groups[10:]
262+
263+
264+
def test_large_group_assignment_split_recursive():
265+
"""
266+
Test group list large enough to trigger recursive split
267+
:return:
268+
"""
269+
group_prefix = "G"
270+
add_groups = [group_prefix+six.text_type(n+1) for n in range(0, 100)]
271+
user = UserAction(id_type=IdentityTypes.enterpriseID, email="[email protected]")
272+
user.add_to_groups(groups=add_groups, group_type=GroupTypes.usergroup)
273+
assert user.maybe_split_groups(10) is True
274+
assert len(user.commands) == 10
275+
276+
277+
def test_large_group_mix_split():
278+
"""
279+
Ensure that group split works on add and remove
280+
Each "add" and "remove" group should be split into 2 groups each
281+
:return:
282+
"""
283+
group_prefix = "G"
284+
add_groups = [group_prefix+six.text_type(n+1) for n in range(0, 15)]
285+
remove_groups = [group_prefix+six.text_type(n+1) for n in range(15, 30)]
286+
user = UserAction(id_type=IdentityTypes.enterpriseID, email="[email protected]")
287+
user.add_to_groups(groups=add_groups, group_type=GroupTypes.usergroup) \
288+
.remove_from_groups(groups=remove_groups, group_type=GroupTypes.usergroup)
289+
assert user.maybe_split_groups(10) is True
290+
assert len(user.commands) == 4
291+
assert user.commands[0]["add"][GroupTypes.usergroup.name] == add_groups[0:10]
292+
assert user.commands[1]["add"][GroupTypes.usergroup.name] == add_groups[10:]
293+
assert user.commands[2]["remove"][GroupTypes.usergroup.name] == remove_groups[0:10]
294+
assert user.commands[3]["remove"][GroupTypes.usergroup.name] == remove_groups[10:]
295+
296+
297+
def test_large_group_action_split():
298+
"""
299+
Ensure that very large group lists (100+) will be handled appropriately
300+
Connection.execute_multiple splits commands and splits actions
301+
Result should be 2 actions, even though we only created one action
302+
:return:
303+
"""
304+
with mock.patch("umapi_client.connection.requests.Session.post") as mock_post:
305+
mock_post.return_value = MockResponse(200, {"result": "success"})
306+
conn = Connection(**mock_connection_params)
307+
308+
group_prefix = "G"
309+
add_groups = [group_prefix+six.text_type(n+1) for n in range(0, 150)]
310+
user = UserAction(id_type=IdentityTypes.enterpriseID, email="[email protected]")
311+
user.add_to_groups(groups=add_groups, group_type=GroupTypes.usergroup)
312+
assert conn.execute_single(user, immediate=True) == (0, 2, 2)
313+
314+
315+
def test_group_size_limit():
316+
"""
317+
Test with different 'throttle_groups' value, which governs the max size of the group list before commands are split
318+
:return:
319+
"""
320+
with mock.patch("umapi_client.connection.requests.Session.post") as mock_post:
321+
mock_post.return_value = MockResponse(200, {"result": "success"})
322+
params = mock_connection_params
323+
params['throttle_groups'] = 5
324+
conn = Connection(**params)
325+
326+
group_prefix = "G"
327+
add_groups = [group_prefix+six.text_type(n+1) for n in range(0, 150)]
328+
user = UserAction(id_type=IdentityTypes.enterpriseID, email="[email protected]")
329+
user.add_to_groups(groups=add_groups, group_type=GroupTypes.usergroup)
330+
assert conn.execute_single(user, immediate=True) == (0, 3, 3)
331+
332+
333+
def test_split_add_user():
334+
"""
335+
Make sure split doesn't do anything when we have a non-add/remove group action
336+
:return:
337+
"""
338+
user = UserAction(id_type=IdentityTypes.enterpriseID, email="[email protected]")
339+
user.create(first_name="Example", last_name="User", country="US", email="[email protected]")
340+
user.update(first_name="EXAMPLE")
341+
assert user.maybe_split_groups(10) is False
342+
assert len(user.commands) == 2
343+
assert user.wire_dict() == {'do': [{'createEnterpriseID': {'country': 'US',
344+
'email': '[email protected]',
345+
'firstname': 'Example',
346+
'lastname': 'User',
347+
'option': 'ignoreIfAlreadyExists'}},
348+
{'update': {'firstname': 'EXAMPLE'}}],
349+
'user': '[email protected]'}
350+
351+
352+
def test_split_role_assignment():
353+
group_prefix = "G"
354+
add_groups = [group_prefix+six.text_type(n+1) for n in range(0, 25)]
355+
user = UserAction(id_type=IdentityTypes.enterpriseID, email="[email protected]")
356+
user.add_role(groups=add_groups, role_type=RoleTypes.admin)
357+
assert user.maybe_split_groups(10) is True
358+
assert len(user.commands) == 3
359+
360+
361+
def test_no_group_split():
362+
"""
363+
maybe_split should return false if nothing was split
364+
:return:
365+
"""
366+
group_prefix = "G"
367+
add_groups = [group_prefix+six.text_type(n+1) for n in range(0, 5)]
368+
user = UserAction(id_type=IdentityTypes.enterpriseID, email="[email protected]")
369+
user.add_to_groups(groups=add_groups, group_type=GroupTypes.usergroup)
370+
assert user.maybe_split_groups(10) is False
371+
assert len(user.commands) == 1
372+
373+
374+
def test_complex_group_split():
375+
"""
376+
Test a complex command with add and remove directive, with multiple group types
377+
UserAction's interface doesn't support this, so we build our own command array
378+
:return:
379+
"""
380+
group_prefix = "G"
381+
add_groups = [group_prefix+six.text_type(n+1) for n in range(0, 150)]
382+
add_products = [group_prefix+six.text_type(n+1) for n in range(0, 26)]
383+
user = UserAction(id_type=IdentityTypes.enterpriseID, email="[email protected]")
384+
user.commands = [{
385+
"add": {
386+
GroupTypes.usergroup.name: add_groups,
387+
GroupTypes.product.name: add_products,
388+
},
389+
}]
390+
assert user.maybe_split_groups(10) is True
391+
assert len(user.commands) == 15
392+
assert len([c for c in user.commands if 'product' in c['add']]) == 3
393+
assert GroupTypes.product.name not in user.commands[3]['add']
394+
395+
396+
def test_split_remove_all():
397+
"""
398+
Don't split groups if "remove" is "all" instead of list
399+
:return:
400+
"""
401+
group_prefix = "G"
402+
add_groups = [group_prefix+six.text_type(n+1) for n in range(0, 11)]
403+
user = UserAction(id_type=IdentityTypes.enterpriseID, email="[email protected]")
404+
user.remove_from_groups(all_groups=True)
405+
assert user.maybe_split_groups(1) is False
406+
assert user.wire_dict() == {'do': [{'remove': 'all'}], 'user': '[email protected]'}
407+
user.add_to_groups(groups=add_groups)
408+
assert user.maybe_split_groups(10) is True
409+
assert user.wire_dict() == {'do': [{'remove': 'all'},
410+
{'add': {'product': ['G1',
411+
'G2',
412+
'G3',
413+
'G4',
414+
'G5',
415+
'G6',
416+
'G7',
417+
'G8',
418+
'G9',
419+
'G10']}},
420+
{'add': {'product': ['G11']}}],
421+
'user': '[email protected]'}

umapi_client/connection.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ def __init__(self,
5555
timeout_seconds=120.0,
5656
throttle_actions=10,
5757
throttle_commands=10,
58+
throttle_groups=10,
5859
user_agent=None,
5960
**kwargs):
6061
"""
@@ -91,6 +92,7 @@ def __init__(self,
9192
:param timeout_seconds: How many seconds to wait for server response (<= 0 or None means forever)
9293
:param throttle_actions: Max number of actions to pack into a single call
9394
:param throttle_commands: Max number of commands allowed in a single action
95+
:param throttle_groups: Max number of groups to add/remove to/from a user
9496
:param user_agent: (optional) string to use as User-Agent header (umapi-client/version data will be added)
9597
9698
Additional keywords are allowed to make it easy to pass a big dictionary with other values
@@ -106,6 +108,7 @@ def __init__(self,
106108
self.timeout = float(timeout_seconds) if timeout_seconds and float(timeout_seconds) > 0.0 else None
107109
self.throttle_actions = max(int(throttle_actions), 1)
108110
self.throttle_commands = max(int(throttle_commands), 1)
111+
self.throttle_groups = max(int(throttle_groups), 1)
109112
self.action_queue = []
110113
self.local_status = {"multiple-query-count": 0,
111114
"single-query-count": 0,
@@ -305,11 +308,20 @@ def execute_multiple(self, actions, immediate=True):
305308
:return: tuple: the number of actions in the queue, that got sent, and that executed successfully.
306309
"""
307310
# throttling part 1: split up each action into smaller actions, as needed
311+
# optionally split large lists of groups in add/remove commands (if action supports it)
308312
split_actions = []
309313
exceptions = []
310314
for a in actions:
311315
if len(a.commands) == 0:
312316
if self.logger: self.logger.warning("Sending action with no commands: %s", a.frame)
317+
# maybe_split_groups is a UserAction attribute, so the call may throw an AttributeError
318+
try:
319+
if a.maybe_split_groups(self.throttle_groups):
320+
if self.logger: self.logger.debug(
321+
"Throttling actions %s to have a maximum of %d entries in group lists.",
322+
a.frame, self.throttle_groups)
323+
except AttributeError:
324+
pass
313325
if len(a.commands) > self.throttle_commands:
314326
if self.logger: self.logger.debug("Throttling action %s to have a maximum of %d commands.",
315327
a.frame, self.throttle_commands)

umapi_client/functional.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -308,6 +308,39 @@ def delete_account(self):
308308
self.append(removeFromDomain={})
309309
return None
310310

311+
def maybe_split_groups(self, max_groups):
312+
"""
313+
Check if group lists in add/remove directives should be split and split them if needed
314+
:param max_groups: Max group list size
315+
:return: True if at least one command was split, False if none were split
316+
"""
317+
split_commands = []
318+
# return True if we split at least once
319+
maybe_split = False
320+
valid_step_keys = ['add', 'addRoles', 'remove']
321+
for command in self.commands:
322+
# commands are assumed to contain a single key
323+
step_key, step_args = next(six.iteritems(command))
324+
if step_key not in valid_step_keys or not isinstance(step_args, dict):
325+
split_commands.append(command)
326+
continue
327+
new_commands = [command]
328+
while True:
329+
new_command = {step_key: {}}
330+
for group_type, groups in six.iteritems(command[step_key]):
331+
if len(groups) > max_groups:
332+
command[step_key][group_type], new_command[step_key][group_type] = \
333+
groups[0:max_groups], groups[max_groups:]
334+
if new_command[step_key]:
335+
new_commands.append(new_command)
336+
command = new_command
337+
maybe_split = True
338+
else:
339+
break
340+
split_commands += new_commands
341+
self.commands = split_commands
342+
return maybe_split
343+
311344

312345
class UsersQuery(QueryMultiple):
313346
"""

umapi_client/version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,4 +18,4 @@
1818
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
1919
# SOFTWARE.
2020

21-
__version__ = "2.8"
21+
__version__ = "2.9"

0 commit comments

Comments
 (0)