Skip to content

Commit 4a80447

Browse files
committed
Add docs for user actions. Improve error summaries. Go to b3.
The user action docs made it clear that we need to show the user as well as the command when summarize an error we got back from the server. So now we do.
1 parent 0e4c241 commit 4a80447

File tree

4 files changed

+177
-6
lines changed

4 files changed

+177
-6
lines changed

docs/usage-instructions-v2.md

Lines changed: 164 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -93,8 +93,14 @@ a Python dictionary of its attributes.
9393

9494
## Get a List of Users
9595

96+
The following code enumerates the first 5 users, printing the email of each.
97+
This will only have fetched the first page of results. Then, after the loop,
98+
the `all_results` call will force the fetch of all remaining pages so the
99+
list of all results can be constructed. ()Once the `all_results` call has been made,
100+
you cannot enumerate again without first calling `reload`.)
101+
96102
```python
97-
users = QueryUsers(conn)
103+
users = umapi_client.QueryUsers(conn)
98104
# print first 5 users
99105
for i, user in enumerate(users):
100106
if i == 5: break
@@ -109,7 +115,7 @@ This list of groups will contain both user groups and product license
109115
configuration groups.
110116

111117
```python
112-
groups = QueryGroups(conn)
118+
groups = umapi_client.QueryGroups(conn)
113119
# print all the group details
114120
for group in groups:
115121
print(group)
@@ -122,5 +128,160 @@ for group in groups:
122128

123129
# Performing Operations on Users
124130

125-
_...under construction..._
131+
User operations in the UMAPI are performed in three steps:
132+
133+
1. You specify the user to be operated on.
134+
2. You specify the operations to be performed on the user.
135+
3. You submit the user and operations to the UMAPI server.
136+
137+
The combined specification of the user identity and the operations to be performed
138+
is called an _action_ in the UMAPI documentation, while the individual operations are called
139+
_commands_. If you read the documentation carefully, you will see that there
140+
are limits to how many actions can be submitted to the UMAPI
141+
service in a single call, how many commands there can be in a single action, and
142+
how many calls can be submitted in a given period of time. However,
143+
the `umapi_client` implementation has been design to insulate
144+
your application from these limits
145+
by
146+
packing as many commands as allowed into each action,
147+
batching up as many actions as possible into a call,
148+
and spacing calls out when required to by the server. Thus, from an
149+
application perspective, you can simply follow the three steps above for each
150+
user and not worry about the mechanics of server communication limits.
151+
152+
## Step 1: Specify the User
153+
154+
To operate on a user, you first create a `UserAction` object that
155+
specifies the user's identity type, domain, and unique ID in the domain.
156+
157+
In most cases,
158+
the user's email ID will be his unique ID and will itself contain his domain,
159+
as in these examples:
160+
161+
```python
162+
from umapi_client import IdentityTypes
163+
user1 = UserAction(id_type=IdentityTypes.adobeID, email="[email protected]")
164+
user2 = UserAction(id_type=IdentityTypes.enterpriseID, email="[email protected]")
165+
```
166+
167+
But when Federated ID is being used, and a non-email username is being
168+
used to identify users across the SAML connection, both the username
169+
and the domain must be specified separately, as in these examples:
170+
171+
```python
172+
user3 = UserAction(id_type=IdentityTypes.federatedID,
173+
username="user347", domain="division.conglomerate.com")
174+
user4 = UserAction(id_type=IdentityTypes.federatedID,
175+
username="user348", domain="division.conglomerate.com",
176+
177+
```
178+
179+
Note that, as in the last example, it's OK to specify the email when
180+
creating a user object even if the email is not the unique ID or
181+
doesn't use the same domain. If
182+
you later perform an operations on a user which requires the email
183+
(such as user creation on the Adobe side), the email will be remembered
184+
and supplied from the UserAction object.
185+
186+
## Step 2: Specify the Operations
187+
188+
Once you have a `UserAction` object for a user, you can specify
189+
operations (called _commands_) to perform on that user. For
190+
example, to create a new user on the Adobe side, for the users
191+
that were specified in the last section, we could do:
192+
193+
```python
194+
user1.create()
195+
user2.create(first_name="Geoffrey", last_name="Giraffe")
196+
user3.create(email="[email protected]", country="US")
197+
user4.create(first_name="John", last_name="User", country="US")
198+
```
199+
200+
When creating users, the email address is mandatory if not already specified
201+
when creating the user action. First and last name and country can be optionally
202+
specified ()except for Adobe ID users),
203+
and country _must_ be specified for Federated ID users.
204+
205+
If a user has already been created, but you want to update attributes,
206+
you can use the `update` rather than the `create` command:
207+
208+
```python
209+
user2.update(first_name="Jeff", country="AU")
210+
user4.update(username="user0347")
211+
```
212+
213+
You can also specify to create if necessary, but update if already created:
214+
215+
```python
216+
from umapi_client import IfAlreadyExistsOptions
217+
user4.create(first_name="John", last_name="User", country="US",
218+
on_conflict=IfAlreadyExistsOptions.updateIfAlreadyExists)
219+
```
220+
221+
There are many other operations you can perform, such as adding and removing
222+
users from user groups and product configuration groups. Because each
223+
operation specifier returns the user, it's easy to chain the together:
224+
225+
```python
226+
user2.add_group(groups=["Photoshop", "Illustrator"]).remove_group(groups=["CC All Apps"])
227+
```
228+
229+
The details of all the possible commands are specified in the code,
230+
and more user documentation will be forthcoming. In general, commands
231+
are performed in the order they are specified, except for certain special
232+
commands such as ```create``` which are always performed first regardless
233+
of when they were specified.
234+
235+
## Step 3: Submit to the UMAPI server
236+
237+
Once you have specified all the desired operations on a given `UserAction`,
238+
you can submit it to the server as follows (recall that `conn` is an authorized
239+
connection to the UMAPI server, as created above):
240+
241+
```python
242+
result = conn.execute_single(user1)
243+
result = conn.execute_multiple([user2, user3, user4])
244+
```
245+
246+
By default, `execute_single` queues the action for sending to the server
247+
when a "full batch" (of 10) actions has been accumulated, but
248+
`execute_multiple` forces a batch to be sent (including any
249+
previously queued actions as well as the specified ones). You can
250+
override these defaults with the `immediate` argument, as in:
251+
252+
```python
253+
result = conn.execute_single(user1, immediate=True)
254+
result = conn.execute_multiple([user2, user3, user4], immediate=False)
255+
```
256+
257+
The result of either execute operation is a tuple of three numbers
258+
`(queued, sent, succeeded)` which tell you how many actions were
259+
queued, how many were sent, and how many of those sent succeeded
260+
without errors. So, for example, in the following code:
261+
262+
```python
263+
queued, _, _ = conn.execute_single(user1)
264+
_, sent, succeeded = conn.execute_multiple(user2, user3, user4)
265+
```
266+
267+
we would likely see `queued = 1`, `sent = 4`, and `succeeded = 4`.
268+
269+
If, for some reason, the succeeded number is not equal to the sent
270+
number for any call, it means that not all of the actions were
271+
executed successfully on the server-side: one or more of the commands
272+
failed to execute. In such cases, the server will have sent back
273+
error information which the `umapi_client` implementation records
274+
against the commands that failed, you can call the `execution_errors`
275+
method on the user actions to get a list of the failed commands
276+
and the server error information. For example, if only three
277+
of the four actions sent had succeeded, then we could execute
278+
this code:
279+
280+
```python
281+
actions = (user1, user2, user3, user4)
282+
errors = [info for action in actions for info in action.execution_errors()]
283+
```
126284

285+
Each entry in errors would then be a dictionary giving
286+
the command that failed, the target user it failed on,
287+
and server information about the reason for the failure.

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
from setuptools import setup, find_packages
2222

2323
setup(name='umapi-client',
24-
version='2.0b2',
24+
version='2.0b3',
2525
description='Client for the User Management API (UMAPI) from Adobe - see https://adobe.ly/2h1pHgV',
2626
long_description=('The User Management API (aka the UMAPI) is an Adobe-hosted network service '
2727
'which provides Adobe Enterprise customers the ability to manage their users. This '

tests/test_actions.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ def test_execute_single_error_queued_throttled():
8787
action = Action(top="top").append(a="a").append(b="b").append(c="c").append(d="d")
8888
assert conn.execute_single(action) == (0, 4, 3)
8989
assert action.execution_errors() == [{"command": {"d": "d"},
90+
"target": {"top": "top"},
9091
"errorCode": "test.error",
9192
"message": "Test error message"}]
9293

@@ -100,7 +101,7 @@ def test_execute_single_error_immediate_throttled():
100101
conn = Connection(throttle_commands=2, **mock_connection_params)
101102
action = Action(top="top0").append(a="a0").append(a="a1").append(a="a2")
102103
assert conn.execute_single(action, immediate=True) == (0, 2, 1)
103-
assert action.execution_errors() == [{"command": {"a": "a2"}, "errorCode": "test"}]
104+
assert action.execution_errors() == [{"command": {"a": "a2"}, "target": {"top": "top0"}, "errorCode": "test"}]
104105

105106

106107
def test_execute_single_dofirst_success_immediate():
@@ -121,6 +122,7 @@ def test_execute_single_error_immediate():
121122
action = Action(top="top").append(a="a")
122123
assert conn.execute_single(action, immediate=True) == (0, 1, 0)
123124
assert action.execution_errors() == [{"command": {"a": "a"},
125+
"target": {"top": "top"},
124126
"errorCode": "test.error",
125127
"message": "Test error message"}]
126128

@@ -138,9 +140,11 @@ def test_execute_single_multi_error_immediate():
138140
action = Action(top="top").append(a="a")
139141
assert conn.execute_single(action, immediate=True) == (0, 1, 0)
140142
assert action.execution_errors() == [{"command": {"a": "a"},
143+
"target": {"top": "top"},
141144
"errorCode": "error1",
142145
"message": "message1"},
143146
{"command": {"a": "a"},
147+
"target": {"top": "top"},
144148
"errorCode": "error2",
145149
"message": "message2"}]
146150

@@ -155,6 +159,7 @@ def test_execute_single_dofirst_error_immediate():
155159
action = Action(top="top").insert(a="a")
156160
assert conn.execute_single(action, immediate=True) == (0, 1, 0)
157161
assert action.execution_errors() == [{"command": {"a": "a"},
162+
"target": {"top": "top"},
158163
"errorCode": "test.error",
159164
"message": "Test error message"}]
160165

@@ -201,6 +206,7 @@ def test_execute_multiple_error():
201206
assert conn.execute_multiple([action0, action1]) == (0, 2, 1)
202207
assert action0.execution_errors() == []
203208
assert action1.execution_errors() == [{"command": {"b": "b"},
209+
"target": {"top": "top1"},
204210
"errorCode": "test.error",
205211
"message": "Test error message"}]
206212

@@ -222,9 +228,11 @@ def test_execute_multiple_multi_error():
222228
assert conn.execute_multiple([action0, action1]) == (0, 2, 1)
223229
assert action0.execution_errors() == []
224230
assert action1.execution_errors() == [{"command": {"b": "b"},
231+
"target": {"top": "top1"},
225232
"errorCode": "error1",
226233
"message": "message1"},
227234
{"command": {"b": "b"},
235+
"target": {"top": "top1"},
228236
"errorCode": "error2",
229237
"message": "message2"}]
230238

@@ -243,6 +251,7 @@ def test_execute_multiple_dofirst_error():
243251
assert conn.execute_multiple([action0, action1]) == (0, 2, 1)
244252
assert action0.execution_errors() == []
245253
assert action1.execution_errors() == [{"command": {"a": "a1"},
254+
"target": {"top": "top1"},
246255
"errorCode": "test.error",
247256
"message": "Test error message"}]
248257

@@ -277,7 +286,7 @@ def test_execute_multiple_single_queued_throttle_actions():
277286
"actions-queued": 0}
278287
assert action0.execution_errors() == []
279288
assert action1.execution_errors() == []
280-
assert action2.execution_errors() == [{"command": {"a": "a2"}, "errorCode": "test"}]
289+
assert action2.execution_errors() == [{"command": {"a": "a2"}, "target": {"top": "top2"}, "errorCode": "test"}]
281290
assert action3.execution_errors() == []
282291

283292

umapi_client/api.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,7 @@ def report_command_error(self, error_dict):
111111
"""
112112
error = dict(error_dict)
113113
error["command"] = self.commands[error_dict["step"]]
114+
error["target"] = self.frame
114115
del error["index"] # throttling can change which action this was in the batch
115116
del error["step"] # throttling can change which step this was in the action
116117
self.errors.append(error)

0 commit comments

Comments
 (0)