Skip to content

Commit 1b19ebf

Browse files
committed
Intermediate state on the way to fixing both #10 and #16. Builds and tests.
* Added functional level users (which do everything) and groups (which just do one query, because that's all the server does). * Added automatic throttling of command count per action and action count per batch to the Connection object. * Added test for throttling, but not any of functional actions yet. I want to rework the way throttling works to make it more of a queueing system.
1 parent 8f2423d commit 1b19ebf

File tree

7 files changed

+433
-107
lines changed

7 files changed

+433
-107
lines changed

setup.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
'cryptography',
4747
'PyJWT',
4848
'six',
49+
'enum34'
4950
],
5051
setup_requires=[
5152
'pytest-runner',

tests/test_actions.py

Lines changed: 39 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -61,15 +61,15 @@ def test_execute_single_success():
6161
mock_post.return_value = MockResponse(200, {"result": "success"})
6262
conn = Connection(**mock_connection_params)
6363
action = Action(top="top").append(a="a")
64-
assert conn.execute_single(action) is True
64+
assert conn.execute_single(action) == (1, 1)
6565

6666

6767
def test_execute_single_dofirst_success():
6868
with mock.patch("umapi_client.connection.requests.post") as mock_post:
6969
mock_post.return_value = MockResponse(200, {"result": "success"})
7070
conn = Connection(**mock_connection_params)
7171
action = Action(top="top").insert(a="a")
72-
assert conn.execute_single(action) is True
72+
assert conn.execute_single(action) == (1, 1)
7373

7474

7575
def test_execute_multiple_success():
@@ -78,7 +78,7 @@ def test_execute_multiple_success():
7878
conn = Connection(**mock_connection_params)
7979
action0 = Action(top="top0").append(a="a0").append(b="b")
8080
action1 = Action(top="top1").append(a="a1")
81-
assert conn.execute_multiple([action0, action1]) == 2
81+
assert conn.execute_multiple([action0, action1]) == (2, 2)
8282

8383

8484
def test_execute_multiple_dofirst_success():
@@ -87,7 +87,7 @@ def test_execute_multiple_dofirst_success():
8787
conn = Connection(**mock_connection_params)
8888
action0 = Action(top="top0").append(a="a0").insert(b="b")
8989
action1 = Action(top="top1").append(a="a1")
90-
assert conn.execute_multiple([action0, action1]) == 2
90+
assert conn.execute_multiple([action0, action1]) == (2, 2)
9191

9292

9393
def test_execute_single_error():
@@ -98,9 +98,8 @@ def test_execute_single_error():
9898
"message": "Test error message"}]})
9999
conn = Connection(**mock_connection_params)
100100
action = Action(top="top").append(a="a")
101-
assert conn.execute_single(action) is False
101+
assert conn.execute_single(action) == (1, 0)
102102
assert action.execution_errors() == [{"command": {"a": "a"},
103-
"step": 0,
104103
"errorCode": "test.error",
105104
"message": "Test error message"}]
106105

@@ -116,13 +115,11 @@ def test_execute_single_multi_error():
116115
"message": "message2"}]})
117116
conn = Connection(**mock_connection_params)
118117
action = Action(top="top").append(a="a")
119-
assert conn.execute_single(action) is False
118+
assert conn.execute_single(action) == (1, 0)
120119
assert action.execution_errors() == [{"command": {"a": "a"},
121-
"step": 0,
122120
"errorCode": "error1",
123121
"message": "message1"},
124122
{"command": {"a": "a"},
125-
"step": 0,
126123
"errorCode": "error2",
127124
"message": "message2"}]
128125

@@ -135,9 +132,8 @@ def test_execute_single_dofirst_error():
135132
"message": "Test error message"}]})
136133
conn = Connection(**mock_connection_params)
137134
action = Action(top="top").insert(a="a")
138-
assert conn.execute_single(action) is False
135+
assert conn.execute_single(action) == (1, 0)
139136
assert action.execution_errors() == [{"command": {"a": "a"},
140-
"step": 0,
141137
"errorCode": "test.error",
142138
"message": "Test error message"}]
143139

@@ -153,10 +149,9 @@ def test_execute_multiple_error():
153149
conn = Connection(**mock_connection_params)
154150
action0 = Action(top="top0").append(a="a0")
155151
action1 = Action(top="top1").append(a="a1").append(b="b")
156-
assert conn.execute_multiple([action0, action1]) == 1
152+
assert conn.execute_multiple([action0, action1]) == (2, 1)
157153
assert action0.execution_errors() == []
158154
assert action1.execution_errors() == [{"command": {"b": "b"},
159-
"step": 1,
160155
"errorCode": "test.error",
161156
"message": "Test error message"}]
162157

@@ -175,14 +170,12 @@ def test_execute_multiple_multi_error():
175170
conn = Connection(**mock_connection_params)
176171
action0 = Action(top="top0").append(a="a0")
177172
action1 = Action(top="top1").append(a="a1").append(b="b")
178-
assert conn.execute_multiple([action0, action1]) == 1
173+
assert conn.execute_multiple([action0, action1]) == (2, 1)
179174
assert action0.execution_errors() == []
180175
assert action1.execution_errors() == [{"command": {"b": "b"},
181-
"step": 1,
182176
"errorCode": "error1",
183177
"message": "message1"},
184178
{"command": {"b": "b"},
185-
"step": 1,
186179
"errorCode": "error2",
187180
"message": "message2"}]
188181

@@ -198,9 +191,37 @@ def test_execute_multiple_dofirst_error():
198191
conn = Connection(**mock_connection_params)
199192
action0 = Action(top="top0").append(a="a0")
200193
action1 = Action(top="top1").append(a="a1").insert(b="b")
201-
assert conn.execute_multiple([action0, action1]) == 1
194+
assert conn.execute_multiple([action0, action1]) == (2, 1)
202195
assert action0.execution_errors() == []
203196
assert action1.execution_errors() == [{"command": {"a": "a1"},
204-
"step": 1,
205197
"errorCode": "test.error",
206198
"message": "Test error message"}]
199+
200+
201+
def test_execute_single_throttle_commands():
202+
with mock.patch("umapi_client.connection.requests.post") as mock_post:
203+
mock_post.return_value = MockResponse(200, {"result": "partial",
204+
"completed": 1,
205+
"notCompleted": 1,
206+
"errors": [{"index": 1, "step": 0, "errorCode": "test"}]})
207+
conn = Connection(throttle_commands=2, **mock_connection_params)
208+
action = Action(top="top0").append(a="a0").append(a="a1").append(a="a2")
209+
assert conn.execute_single(action) == (2, 1)
210+
assert action.execution_errors() == [{"command": {"a": "a2"}, "errorCode": "test"}]
211+
212+
213+
def test_execute_multiple_throttle_actions():
214+
with mock.patch("umapi_client.connection.requests.post") as mock_post:
215+
mock_post.side_effect = [MockResponse(200, {"result": "success"}),
216+
MockResponse(200, {"result": "partial",
217+
"completed": 0,
218+
"notCompleted": 1,
219+
"errors": [{"index": 0, "step": 0, "errorCode": "test"}]})]
220+
conn = Connection(throttle_actions=2, **mock_connection_params)
221+
action0 = Action(top="top0").append(a="a0")
222+
action1 = Action(top="top1").append(a="a1")
223+
action2 = Action(top="top2").append(a="a2")
224+
assert conn.execute_multiple([action0, action1, action2]) == (3, 2)
225+
assert action0.execution_errors() == []
226+
assert action1.execution_errors() == []
227+
assert action2.execution_errors() == [{"command": {"a": "a2"}, "errorCode": "test"}]

umapi_client/__init__.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,5 +19,7 @@
1919
# SOFTWARE.
2020

2121
from .connection import Connection
22-
from .api import Action, UserAction, QueryMultiple, UsersQuery, GroupsQuery, QuerySingle, UserQuery
22+
from .api import Action, QuerySingle, QueryMultiple
23+
from .users import UserAction, UserQuery, UsersQuery, IdentityTypes, GroupTypes, RoleTypes
24+
from .groups import GroupsQuery
2325
from .error import ClientError, RequestError, ServerError, UnavailableError

umapi_client/api.py

Lines changed: 23 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,23 @@ def __init__(self, **kwargs):
4141
self.frame = dict(kwargs)
4242
self.commands = []
4343
self.errors = []
44+
self.split_actions = None
45+
46+
def split(self, max_commands):
47+
"""
48+
Split this action into an equivalent list of actions, each of which have at most max_commands commands.
49+
:param max_commands: max number of commands allowed in any action
50+
:return: the list of commands created from this one
51+
"""
52+
prior = Action(**self.frame)
53+
prior.commands = list(self.commands)
54+
self.split_actions = [prior]
55+
while len(prior.commands) > max_commands:
56+
next = Action(**self.frame)
57+
prior.commands, next.commands = prior.commands[0:max_commands], prior.commands[max_commands:]
58+
self.split_actions.append(next)
59+
prior = next
60+
return self.split_actions
4461

4562
def wire_dict(self):
4663
"""
@@ -94,7 +111,8 @@ def report_command_error(self, error_dict):
94111
"""
95112
error = dict(error_dict)
96113
error["command"] = self.commands[error_dict["step"]]
97-
del error["index"] # doesn't matter which action this was in the server-sent batch
114+
del error["index"] # throttling can change which action this was in the batch
115+
del error["step"] # throttling can change which step this was in the action
98116
self.errors.append(error)
99117

100118
def execution_errors(self):
@@ -104,38 +122,11 @@ def execution_errors(self):
104122
Each dictionary entry gives the command dictionary and the error dictionary
105123
:return: list of commands that gave errors, with their error information
106124
"""
107-
return [dict(e) for e in self.errors]
108-
109-
110-
class UserAction(Action):
111-
"""
112-
An sequence of commands for the UMAPI to perform on a single user.
113-
"""
114-
115-
def __init__(self, email=None, username=None, domain=None, **kwargs):
116-
"""
117-
Create an Action for a user identified either by email or by username and domain.
118-
There is never a reason to specify both email and username.
119-
:param username: string, username in the Adobe domain (might be email)
120-
:param domain: string, required if the username is not an email address
121-
:param kwargs: other key/value pairs desired to identify this user
122-
"""
123-
if email:
124-
if username or domain:
125-
ValueError("User create: Email was specified; don't also specify username or domain")
126-
if not re.match(r"^[a-zA-Z0-9!#$%&'*+/=?^_`{|}~;-]+([.][a-zA-Z0-9!#$%&'*+/=?^_`{|}~;-]+)*"
127-
r"@"
128-
r"[a-zA-Z0-9-]+([.][a-zA-Z0-9-]+)+$", email):
129-
ValueError("Action create: Illegal email format")
130-
Action.__init__(user=email, **kwargs)
125+
if self.split_actions:
126+
# throttling split this action, get errors from the split
127+
return [dict(e) for s in self.split_actions for e in s.errors]
131128
else:
132-
if not username or not domain:
133-
ValueError("User create: Both username and domain must be specified")
134-
if not re.match(r"^[a-zA-Z0-9!#$%&'*+/=?^_`{|}~;-]+([.][a-zA-Z0-9!#$%&'*+/=?^_`{|}~;-]+)*$", username):
135-
ValueError("User create: Illegal characters in username")
136-
if not re.match(r"^[a-zA-Z0-9-]+([.][a-zA-Z0-9-]+)+$", domain):
137-
ValueError("User create: Illegal domain format")
138-
Action.__init__(user=username, domain=domain, **kwargs)
129+
return [dict(e) for e in self.errors]
139130

140131

141132
class QueryMultiple:
@@ -228,37 +219,6 @@ def all_results(self):
228219
return list(self._results)
229220

230221

231-
class UsersQuery(QueryMultiple):
232-
"""
233-
Query for users meeting (optional) criteria
234-
"""
235-
236-
def __init__(self, connection, in_group="", in_domain="", identity_type=""):
237-
"""
238-
Create a query for all users, or for those in a group or domain or both
239-
:param connection: Connection to run the query against
240-
:param in_group: (optional) name of the group to restrict the query to
241-
:param in_domain: (optional) name of the domain to restrict the query to
242-
"""
243-
groups = [in_group] if in_group else []
244-
params = {}
245-
if in_domain: params["domain"] = str(in_domain)
246-
if identity_type: params["type"] = str(identity_type)
247-
QueryMultiple.__init__(self, connection=connection, object_type="user", url_params=groups, query_params=params)
248-
249-
250-
class GroupsQuery(QueryMultiple):
251-
"""
252-
Query for all groups
253-
"""
254-
255-
def __init__(self, connection):
256-
"""
257-
Create a query for all groups
258-
:param connection: Connection to run the query against
259-
"""
260-
QueryMultiple.__init__(self, connection=connection, object_type="group")
261-
262222
class QuerySingle:
263223
"""
264224
Look for a single object
@@ -301,16 +261,3 @@ def result(self):
301261
if self._result is None:
302262
self._fetch_result()
303263
return self._result
304-
305-
class UserQuery(QuerySingle):
306-
"""
307-
Query for a single user
308-
"""
309-
310-
def __init__(self, connection, email):
311-
"""
312-
Create a query for the user with the given email
313-
:param connection: Connection to run the query against
314-
:param email: email of user to query for
315-
"""
316-
QuerySingle.__init__(self, connection=connection, object_type="user", url_params=[str(email)])

0 commit comments

Comments
 (0)