pyxecm 1.4__py3-none-any.whl → 1.5__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of pyxecm might be problematic. Click here for more details.
- pyxecm/__init__.py +3 -0
- pyxecm/coreshare.py +2636 -0
- pyxecm/customizer/__init__.py +4 -0
- pyxecm/customizer/browser_automation.py +164 -54
- pyxecm/customizer/customizer.py +451 -235
- pyxecm/customizer/k8s.py +6 -6
- pyxecm/customizer/m365.py +1136 -221
- pyxecm/customizer/payload.py +13163 -5844
- pyxecm/customizer/pht.py +503 -0
- pyxecm/customizer/salesforce.py +694 -114
- pyxecm/customizer/sap.py +4 -4
- pyxecm/customizer/servicenow.py +1221 -0
- pyxecm/customizer/successfactors.py +1056 -0
- pyxecm/helper/__init__.py +2 -0
- pyxecm/helper/assoc.py +24 -1
- pyxecm/helper/data.py +1527 -0
- pyxecm/helper/web.py +170 -46
- pyxecm/helper/xml.py +170 -34
- pyxecm/otac.py +309 -23
- pyxecm/otcs.py +2779 -698
- pyxecm/otds.py +347 -108
- pyxecm/otmm.py +808 -0
- pyxecm/otpd.py +13 -10
- {pyxecm-1.4.dist-info → pyxecm-1.5.dist-info}/METADATA +3 -1
- pyxecm-1.5.dist-info/RECORD +30 -0
- {pyxecm-1.4.dist-info → pyxecm-1.5.dist-info}/WHEEL +1 -1
- pyxecm-1.4.dist-info/RECORD +0 -24
- {pyxecm-1.4.dist-info → pyxecm-1.5.dist-info}/LICENSE +0 -0
- {pyxecm-1.4.dist-info → pyxecm-1.5.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,1056 @@
|
|
|
1
|
+
"""
|
|
2
|
+
SuccessFactors Module to interact with the SuccessFactors API
|
|
3
|
+
|
|
4
|
+
See:
|
|
5
|
+
https://community.sap.com/t5/enterprise-resource-planning-blogs-by-members/how-to-initiate-an-oauth-connection-to-successfactors-employee-central/ba-p/13332388
|
|
6
|
+
https://help.sap.com/docs/SAP_SUCCESSFACTORS_PLATFORM/d599f15995d348a1b45ba5603e2aba9b/78b1d8aac783455684a7de7a8a5b0c04.html
|
|
7
|
+
|
|
8
|
+
Class: SuccessFactors
|
|
9
|
+
Methods:
|
|
10
|
+
|
|
11
|
+
__init__ : class initializer
|
|
12
|
+
config : Returns config data set
|
|
13
|
+
credentials: Returns the token data
|
|
14
|
+
idp_data: Return the IDP data used to request the SAML assertion
|
|
15
|
+
request_header: Returns the request header for SuccessFactors API calls
|
|
16
|
+
parse_request_response: Parse the REST API responses and convert
|
|
17
|
+
them to Python dict in a safe way
|
|
18
|
+
exist_result_item: Check if an dict item is in the response
|
|
19
|
+
of the SuccessFactors API call
|
|
20
|
+
get_result_value: Check if a defined value (based on a key) is in the SuccessFactors API response
|
|
21
|
+
|
|
22
|
+
get_saml_assertion: Get SAML Assertion for SuccessFactors authentication
|
|
23
|
+
authenticate : Authenticates at SuccessFactors API
|
|
24
|
+
|
|
25
|
+
get_country: Get information for a Country / Countries
|
|
26
|
+
get_user: Get a SuccessFactors user based on its ID.
|
|
27
|
+
get_user_account: Get information for a SuccessFactors User Account
|
|
28
|
+
update_user: Update user data. E.g. update the user password or email.
|
|
29
|
+
get_employee: Get a list of employee(s) matching given criterias.
|
|
30
|
+
get_entities_metadata: Get the schema (metadata) for a list of entities
|
|
31
|
+
(list can be empty to get it for all)
|
|
32
|
+
get_entity_metadata: Get the schema (metadata) for an entity
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
__author__ = "Dr. Marc Diefenbruch"
|
|
36
|
+
__copyright__ = "Copyright 2024, OpenText"
|
|
37
|
+
__credits__ = ["Kai-Philip Gatzweiler"]
|
|
38
|
+
__maintainer__ = "Dr. Marc Diefenbruch"
|
|
39
|
+
__email__ = "mdiefenb@opentext.com"
|
|
40
|
+
|
|
41
|
+
import json
|
|
42
|
+
import logging
|
|
43
|
+
import time
|
|
44
|
+
import urllib.parse
|
|
45
|
+
import requests
|
|
46
|
+
|
|
47
|
+
import xmltodict
|
|
48
|
+
|
|
49
|
+
logger = logging.getLogger("pyxecm.customizer.sucessfactors")
|
|
50
|
+
|
|
51
|
+
request_login_headers = {
|
|
52
|
+
"Content-Type": "application/x-www-form-urlencoded", # "application/json",
|
|
53
|
+
"Accept": "application/json",
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
REQUEST_TIMEOUT = 60
|
|
57
|
+
REQUEST_MAX_RETRIES = 5
|
|
58
|
+
REQUEST_RETRY_DELAY = 60
|
|
59
|
+
|
|
60
|
+
class SuccessFactors(object):
|
|
61
|
+
"""Used to retrieve and automate stettings in SuccessFactors."""
|
|
62
|
+
|
|
63
|
+
_config: dict
|
|
64
|
+
_access_token = None
|
|
65
|
+
_assertion = None
|
|
66
|
+
|
|
67
|
+
def __init__(
|
|
68
|
+
self,
|
|
69
|
+
base_url: str,
|
|
70
|
+
as_url: str,
|
|
71
|
+
client_id: str,
|
|
72
|
+
client_secret: str,
|
|
73
|
+
username: str = "",
|
|
74
|
+
password: str = "",
|
|
75
|
+
company_id: str = "",
|
|
76
|
+
authorization_url: str = "",
|
|
77
|
+
):
|
|
78
|
+
"""Initialize the SuccessFactors object
|
|
79
|
+
|
|
80
|
+
Args:
|
|
81
|
+
base_url (str): base URL of the SuccessFactors tenant
|
|
82
|
+
authorization_url (str): authorization URL of the SuccessFactors tenant, typically ending with "/services/oauth2/token"
|
|
83
|
+
client_id (str): SuccessFactors Client ID
|
|
84
|
+
client_secret (str): SuccessFactors Client Secret
|
|
85
|
+
username (str): user name in SuccessFactors
|
|
86
|
+
password (str): password of the user
|
|
87
|
+
authorization_url (str, optional): URL for SuccessFactors login. If not given it will be constructed with default values
|
|
88
|
+
using base_url
|
|
89
|
+
"""
|
|
90
|
+
|
|
91
|
+
successfactors_config = {}
|
|
92
|
+
|
|
93
|
+
# this class assumes that the base URL is provided without
|
|
94
|
+
# a trailing "/". Otherwise the trailing slash is removed.
|
|
95
|
+
if base_url.endswith("/"):
|
|
96
|
+
base_url = base_url[:-1]
|
|
97
|
+
|
|
98
|
+
# Set the authentication endpoints and credentials
|
|
99
|
+
successfactors_config["baseUrl"] = base_url
|
|
100
|
+
successfactors_config["asUrl"] = as_url
|
|
101
|
+
successfactors_config["clientId"] = client_id
|
|
102
|
+
successfactors_config["clientSecret"] = client_secret
|
|
103
|
+
successfactors_config["username"] = username.split("@")[
|
|
104
|
+
0
|
|
105
|
+
] # we don't want the company ID in the user name
|
|
106
|
+
successfactors_config["password"] = password
|
|
107
|
+
if company_id:
|
|
108
|
+
successfactors_config["companyId"] = company_id
|
|
109
|
+
elif "@" in username:
|
|
110
|
+
# if the company ID is not provided as a parameter
|
|
111
|
+
# we check if it is included in the username:
|
|
112
|
+
company_id = username.split("@")[1]
|
|
113
|
+
successfactors_config["companyId"] = company_id
|
|
114
|
+
if authorization_url:
|
|
115
|
+
successfactors_config["authenticationUrl"] = authorization_url
|
|
116
|
+
else:
|
|
117
|
+
successfactors_config["authenticationUrl"] = (
|
|
118
|
+
successfactors_config["baseUrl"] + "/oauth/token"
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
successfactors_config["idpUrl"] = (
|
|
122
|
+
successfactors_config["baseUrl"] + "/oauth/idp"
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
if not username:
|
|
126
|
+
# Set the data for the token request
|
|
127
|
+
successfactors_config["authenticationData"] = {
|
|
128
|
+
"grant_type": "client_credentials",
|
|
129
|
+
"client_id": client_id,
|
|
130
|
+
"client_secret": client_secret,
|
|
131
|
+
# "username": successfactors_config["username"],
|
|
132
|
+
# "password": password,
|
|
133
|
+
}
|
|
134
|
+
else:
|
|
135
|
+
# Set the data for the token request
|
|
136
|
+
successfactors_config["authenticationData"] = {
|
|
137
|
+
# "grant_type": "password",
|
|
138
|
+
"grant_type": "urn:ietf:params:oauth:grant-type:saml2-bearer",
|
|
139
|
+
"company_id": successfactors_config["companyId"],
|
|
140
|
+
"username": successfactors_config["username"],
|
|
141
|
+
"password": password,
|
|
142
|
+
"client_id": client_id,
|
|
143
|
+
"client_secret": client_secret,
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
successfactors_config["idpData"] = {
|
|
147
|
+
"client_id": client_id,
|
|
148
|
+
"user_id": successfactors_config["username"],
|
|
149
|
+
# "use_email": True,
|
|
150
|
+
"token_url": successfactors_config["authenticationUrl"],
|
|
151
|
+
"private_key": client_secret,
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
self._config = successfactors_config
|
|
155
|
+
|
|
156
|
+
# end method definition
|
|
157
|
+
|
|
158
|
+
def config(self) -> dict:
|
|
159
|
+
"""Returns the configuration dictionary
|
|
160
|
+
|
|
161
|
+
Returns:
|
|
162
|
+
dict: Configuration dictionary
|
|
163
|
+
"""
|
|
164
|
+
return self._config
|
|
165
|
+
|
|
166
|
+
# end method definition
|
|
167
|
+
|
|
168
|
+
def credentials(self) -> dict:
|
|
169
|
+
"""Return the login credentials
|
|
170
|
+
|
|
171
|
+
Returns:
|
|
172
|
+
dict: dictionary with login credentials for SuccessFactors
|
|
173
|
+
"""
|
|
174
|
+
return self.config()["authenticationData"]
|
|
175
|
+
|
|
176
|
+
# end method definition
|
|
177
|
+
|
|
178
|
+
def idp_data(self) -> dict:
|
|
179
|
+
"""Return the IDP data used to request the SAML assertion
|
|
180
|
+
|
|
181
|
+
Returns:
|
|
182
|
+
dict: dictionary with IDP data for SuccessFactors
|
|
183
|
+
"""
|
|
184
|
+
return self.config()["idpData"]
|
|
185
|
+
|
|
186
|
+
# end method definition
|
|
187
|
+
|
|
188
|
+
def request_header(self, content_type: str = "application/json") -> dict:
|
|
189
|
+
"""Returns the request header used for Application calls.
|
|
190
|
+
Consists of Bearer access token and Content Type
|
|
191
|
+
|
|
192
|
+
Args:
|
|
193
|
+
content_type (str, optional): content type for the request
|
|
194
|
+
Return:
|
|
195
|
+
dict: request header values
|
|
196
|
+
"""
|
|
197
|
+
|
|
198
|
+
request_header = {
|
|
199
|
+
"Authorization": "Bearer {}".format(self._access_token),
|
|
200
|
+
"Content-Type": content_type,
|
|
201
|
+
"Accept": content_type,
|
|
202
|
+
}
|
|
203
|
+
return request_header
|
|
204
|
+
|
|
205
|
+
# end method definition
|
|
206
|
+
|
|
207
|
+
def parse_request_response(
|
|
208
|
+
self,
|
|
209
|
+
response_object: requests.Response,
|
|
210
|
+
additional_error_message: str = "",
|
|
211
|
+
show_error: bool = True,
|
|
212
|
+
) -> dict | None:
|
|
213
|
+
"""Converts the request response (JSon) to a Python dict in a safe way
|
|
214
|
+
that also handles exceptions. It first tries to load the response.text
|
|
215
|
+
via json.loads() that produces a dict output. Only if response.text is
|
|
216
|
+
not set or is empty it just converts the response_object to a dict using
|
|
217
|
+
the vars() built-in method.
|
|
218
|
+
|
|
219
|
+
Args:
|
|
220
|
+
response_object (object): this is reponse object delivered by the request call
|
|
221
|
+
additional_error_message (str, optional): use a more specific error message
|
|
222
|
+
in case of an error
|
|
223
|
+
show_error (bool): True: write an error to the log file
|
|
224
|
+
False: write a warning to the log file
|
|
225
|
+
Returns:
|
|
226
|
+
dict: response information or None in case of an error
|
|
227
|
+
"""
|
|
228
|
+
|
|
229
|
+
if not response_object:
|
|
230
|
+
return None
|
|
231
|
+
|
|
232
|
+
try:
|
|
233
|
+
if response_object.text:
|
|
234
|
+
dict_object = json.loads(response_object.text)
|
|
235
|
+
else:
|
|
236
|
+
dict_object = vars(response_object)
|
|
237
|
+
except json.JSONDecodeError as exception:
|
|
238
|
+
if additional_error_message:
|
|
239
|
+
message = "Cannot decode response as JSon. {}; error -> {}".format(
|
|
240
|
+
additional_error_message, exception
|
|
241
|
+
)
|
|
242
|
+
else:
|
|
243
|
+
message = "Cannot decode response as JSon; error -> {}".format(
|
|
244
|
+
exception
|
|
245
|
+
)
|
|
246
|
+
if show_error:
|
|
247
|
+
logger.error(message)
|
|
248
|
+
else:
|
|
249
|
+
logger.warning(message)
|
|
250
|
+
return None
|
|
251
|
+
else:
|
|
252
|
+
return dict_object
|
|
253
|
+
|
|
254
|
+
# end method definition
|
|
255
|
+
|
|
256
|
+
def exist_result_item(self, response: dict, key: str, value: str) -> bool:
|
|
257
|
+
"""Check existence of key / value pair in the response properties of an SuccessFactors API call.
|
|
258
|
+
|
|
259
|
+
Args:
|
|
260
|
+
response (dict): REST response from an SuccessFactors API call
|
|
261
|
+
key (str): property name (key)
|
|
262
|
+
value (str): value to find in the item with the matching key
|
|
263
|
+
Returns:
|
|
264
|
+
bool: True if the value was found, False otherwise
|
|
265
|
+
"""
|
|
266
|
+
|
|
267
|
+
if not response:
|
|
268
|
+
return False
|
|
269
|
+
|
|
270
|
+
if "d" in response:
|
|
271
|
+
data = response["d"]
|
|
272
|
+
if not key in data:
|
|
273
|
+
return False
|
|
274
|
+
if value == data[key]:
|
|
275
|
+
return True
|
|
276
|
+
else:
|
|
277
|
+
if not key in response:
|
|
278
|
+
return False
|
|
279
|
+
if value == response[key]:
|
|
280
|
+
return True
|
|
281
|
+
|
|
282
|
+
return False
|
|
283
|
+
|
|
284
|
+
# end method definition
|
|
285
|
+
|
|
286
|
+
def get_result_value(
|
|
287
|
+
self,
|
|
288
|
+
response: dict,
|
|
289
|
+
key: str,
|
|
290
|
+
index: int = 0,
|
|
291
|
+
) -> str | None:
|
|
292
|
+
"""Get value of a result property with a given key of an SuccessFactors API call.
|
|
293
|
+
|
|
294
|
+
Args:
|
|
295
|
+
response (dict): REST response from an SuccessFactors REST Call
|
|
296
|
+
key (str): property name (key)
|
|
297
|
+
index (int, optional): Index to use (1st element has index 0).
|
|
298
|
+
Defaults to 0.
|
|
299
|
+
Returns:
|
|
300
|
+
str: value for the key, None otherwise
|
|
301
|
+
"""
|
|
302
|
+
|
|
303
|
+
if not response or not "d" in response:
|
|
304
|
+
return None
|
|
305
|
+
|
|
306
|
+
data = response["d"]
|
|
307
|
+
|
|
308
|
+
# list response types are wrapped in a "results" element
|
|
309
|
+
# which is of type list
|
|
310
|
+
if "results" in data:
|
|
311
|
+
results = data["results"]
|
|
312
|
+
if not results or not isinstance(results, list):
|
|
313
|
+
return None
|
|
314
|
+
try:
|
|
315
|
+
value = results[index][key]
|
|
316
|
+
except IndexError as e:
|
|
317
|
+
logger.error(
|
|
318
|
+
"Index error with index -> %s and key -> %s: %s",
|
|
319
|
+
str(index),
|
|
320
|
+
key,
|
|
321
|
+
str(e),
|
|
322
|
+
)
|
|
323
|
+
return None
|
|
324
|
+
except KeyError as e:
|
|
325
|
+
logger.error(
|
|
326
|
+
"Key error with index -> %s and key -> %s: %s",
|
|
327
|
+
str(index),
|
|
328
|
+
key,
|
|
329
|
+
str(e),
|
|
330
|
+
)
|
|
331
|
+
return None
|
|
332
|
+
else: # simple response - try to find key in response directly:
|
|
333
|
+
if not key in data:
|
|
334
|
+
return None
|
|
335
|
+
value = data[key]
|
|
336
|
+
|
|
337
|
+
return value
|
|
338
|
+
|
|
339
|
+
# end method definition
|
|
340
|
+
|
|
341
|
+
def get_saml_assertion(self) -> str | None:
|
|
342
|
+
"""Get SAML Assertion for SuccessFactors authentication.
|
|
343
|
+
|
|
344
|
+
Args:
|
|
345
|
+
None
|
|
346
|
+
Returns:
|
|
347
|
+
str: Assertion. Also stores access token in self._assertion. None in case of error
|
|
348
|
+
"""
|
|
349
|
+
|
|
350
|
+
request_url = self.config()["idpUrl"]
|
|
351
|
+
|
|
352
|
+
# request_header = request_login_headers
|
|
353
|
+
|
|
354
|
+
logger.debug("Requesting SuccessFactors SAML Assertion from -> %s", request_url)
|
|
355
|
+
|
|
356
|
+
idp_post_body = self.config()["idpData"]
|
|
357
|
+
|
|
358
|
+
response = None
|
|
359
|
+
self._assertion = None
|
|
360
|
+
|
|
361
|
+
try:
|
|
362
|
+
response = requests.post(
|
|
363
|
+
request_url,
|
|
364
|
+
data=idp_post_body,
|
|
365
|
+
# headers=request_header,
|
|
366
|
+
timeout=REQUEST_TIMEOUT,
|
|
367
|
+
)
|
|
368
|
+
except requests.exceptions.ConnectionError as exception:
|
|
369
|
+
logger.error(
|
|
370
|
+
"Unable to get SAML assertion from -> %s : %s",
|
|
371
|
+
self.config()["idpUrl"],
|
|
372
|
+
exception,
|
|
373
|
+
)
|
|
374
|
+
return None
|
|
375
|
+
|
|
376
|
+
if response.ok:
|
|
377
|
+
assertion = response.text
|
|
378
|
+
self._assertion = assertion
|
|
379
|
+
logger.debug("Assertion -> %s", self._assertion)
|
|
380
|
+
return assertion
|
|
381
|
+
|
|
382
|
+
logger.error(
|
|
383
|
+
"Failed to request an SuccessFactors SAML Assertion; error -> %s",
|
|
384
|
+
response.text,
|
|
385
|
+
)
|
|
386
|
+
return None
|
|
387
|
+
|
|
388
|
+
# end method definition
|
|
389
|
+
|
|
390
|
+
def authenticate(self, revalidate: bool = False) -> str | None:
|
|
391
|
+
"""Authenticate at SuccessFactors with client ID and client secret.
|
|
392
|
+
|
|
393
|
+
Args:
|
|
394
|
+
revalidate (bool, optional): determinse if a re-athentication is enforced
|
|
395
|
+
(e.g. if session has timed out with 401 error)
|
|
396
|
+
Returns:
|
|
397
|
+
str: Access token. Also stores access token in self._access_token. None in case of error
|
|
398
|
+
"""
|
|
399
|
+
|
|
400
|
+
if not self._assertion:
|
|
401
|
+
self._assertion = self.get_saml_assertion()
|
|
402
|
+
|
|
403
|
+
# Already authenticated and session still valid?
|
|
404
|
+
if self._access_token and not revalidate:
|
|
405
|
+
logger.debug(
|
|
406
|
+
"Session still valid - return existing access token -> %s",
|
|
407
|
+
str(self._access_token),
|
|
408
|
+
)
|
|
409
|
+
return self._access_token
|
|
410
|
+
|
|
411
|
+
request_url = self.config()["authenticationUrl"]
|
|
412
|
+
|
|
413
|
+
# request_header = request_login_headers
|
|
414
|
+
|
|
415
|
+
logger.debug("Requesting SuccessFactors Access Token from -> %s", request_url)
|
|
416
|
+
|
|
417
|
+
authenticate_post_body = self.credentials()
|
|
418
|
+
authenticate_post_body["assertion"] = self._assertion
|
|
419
|
+
|
|
420
|
+
response = None
|
|
421
|
+
self._access_token = None
|
|
422
|
+
|
|
423
|
+
try:
|
|
424
|
+
response = requests.post(
|
|
425
|
+
request_url,
|
|
426
|
+
data=authenticate_post_body,
|
|
427
|
+
# headers=request_header,
|
|
428
|
+
timeout=REQUEST_TIMEOUT,
|
|
429
|
+
)
|
|
430
|
+
except requests.exceptions.ConnectionError as exception:
|
|
431
|
+
logger.warning(
|
|
432
|
+
"Unable to connect to -> %s : %s",
|
|
433
|
+
self.config()["authenticationUrl"],
|
|
434
|
+
exception,
|
|
435
|
+
)
|
|
436
|
+
return None
|
|
437
|
+
|
|
438
|
+
if response.ok:
|
|
439
|
+
authenticate_dict = self.parse_request_response(response)
|
|
440
|
+
if not authenticate_dict or not "access_token" in authenticate_dict:
|
|
441
|
+
return None
|
|
442
|
+
# Store authentication access_token:
|
|
443
|
+
self._access_token = authenticate_dict["access_token"]
|
|
444
|
+
logger.debug("Access Token -> %s", self._access_token)
|
|
445
|
+
else:
|
|
446
|
+
logger.error(
|
|
447
|
+
"Failed to request an SuccessFactors Access Token; error -> %s",
|
|
448
|
+
response.text,
|
|
449
|
+
)
|
|
450
|
+
return None
|
|
451
|
+
|
|
452
|
+
return self._access_token
|
|
453
|
+
|
|
454
|
+
# end method definition
|
|
455
|
+
|
|
456
|
+
def get_country(self, code: str = "") -> dict | None:
|
|
457
|
+
"""Get information for a Country / Countries
|
|
458
|
+
|
|
459
|
+
Args:
|
|
460
|
+
code (str): 3 character code for contry selection, like "USA"
|
|
461
|
+
|
|
462
|
+
Returns:
|
|
463
|
+
dict | None: Country details
|
|
464
|
+
|
|
465
|
+
Example return data in "d" dictionary:
|
|
466
|
+
|
|
467
|
+
{
|
|
468
|
+
'__metadata': {
|
|
469
|
+
'uri': "https://apisalesdemo2.successfactors.eu/odata/v2/UserAccount('twalker')",
|
|
470
|
+
'type': 'SFOData.UserAccount'
|
|
471
|
+
},
|
|
472
|
+
'username': 'twalker',
|
|
473
|
+
'lastModifiedDateTime': '/Date(1692701804000+0000)/',
|
|
474
|
+
'accountUuid': '5c7390e0-d9d2-e348-1700-2b02b3a61aa5',
|
|
475
|
+
'createdDateTime': '/Date(1420745485000+0000)/',
|
|
476
|
+
'timeZone': 'US/Eastern',
|
|
477
|
+
'lastInactivationDateTime': None,
|
|
478
|
+
'accountIsLocked': 'FALSE',
|
|
479
|
+
'accountStatus': 'ACTIVE',
|
|
480
|
+
'defaultLocale': 'en_US',
|
|
481
|
+
'lastLoginFailedDateTime': None,
|
|
482
|
+
'accountId': '90',
|
|
483
|
+
'sapGlobalUserId': None,
|
|
484
|
+
'personIdExternal': '82094',
|
|
485
|
+
'userType': 'employee',
|
|
486
|
+
'email': 'twalker@m365x41497014.onmicrosoft.com',
|
|
487
|
+
'user': {'__deferred': {...}}
|
|
488
|
+
}
|
|
489
|
+
"""
|
|
490
|
+
|
|
491
|
+
if not self._access_token:
|
|
492
|
+
self.authenticate()
|
|
493
|
+
|
|
494
|
+
if code:
|
|
495
|
+
request_url = self.config()["asUrl"] + "Country(code='{}')".format(
|
|
496
|
+
code
|
|
497
|
+
) # ,effectiveStartDate=datetime'1900-01-01T00:00:00'
|
|
498
|
+
else:
|
|
499
|
+
request_url = self.config()["asUrl"] + "Country"
|
|
500
|
+
|
|
501
|
+
request_header = self.request_header()
|
|
502
|
+
|
|
503
|
+
response = requests.get(
|
|
504
|
+
request_url, headers=request_header, timeout=REQUEST_TIMEOUT
|
|
505
|
+
)
|
|
506
|
+
if response.status_code == 200:
|
|
507
|
+
return self.parse_request_response(response)
|
|
508
|
+
else:
|
|
509
|
+
logger.error(
|
|
510
|
+
"Failed to retrieve country data; status -> %s; error -> %s",
|
|
511
|
+
response.status_code,
|
|
512
|
+
response.text,
|
|
513
|
+
)
|
|
514
|
+
return None
|
|
515
|
+
|
|
516
|
+
# end method definition
|
|
517
|
+
|
|
518
|
+
def get_user(
|
|
519
|
+
self,
|
|
520
|
+
user_id: str = "", # this is NOT the username but really an ID like 106020
|
|
521
|
+
field_name: str = "",
|
|
522
|
+
field_value: str = "",
|
|
523
|
+
field_operation: str = "eq",
|
|
524
|
+
max_results: int = 1,
|
|
525
|
+
) -> dict | None:
|
|
526
|
+
"""Get information for a User Account
|
|
527
|
+
Inactive users are not returned by default. To query inactive users,
|
|
528
|
+
you can explicitly include the status in a $filter or use a key predicate.
|
|
529
|
+
If you want to query all users, use query option $filter=status in 't','f','T','F','e','d'.
|
|
530
|
+
|
|
531
|
+
Args:
|
|
532
|
+
user_id (str): login name of the user (e.g. "twalker")
|
|
533
|
+
|
|
534
|
+
Returns:
|
|
535
|
+
dict | None: User Account details
|
|
536
|
+
|
|
537
|
+
Example return data in "d" dictionary:
|
|
538
|
+
|
|
539
|
+
{
|
|
540
|
+
'__metadata': {
|
|
541
|
+
'uri': "https://apisalesdemo2.successfactors.eu/odata/v2/User('106020')",
|
|
542
|
+
'type': 'SFOData.User'
|
|
543
|
+
},
|
|
544
|
+
'userId': '106020',
|
|
545
|
+
'salaryBudgetFinalSalaryPercentage': None,
|
|
546
|
+
'dateOfCurrentPosition': '/Date(1388534400000)/',
|
|
547
|
+
'matrix1Label': None,
|
|
548
|
+
'salary': '79860.0',
|
|
549
|
+
'objective': '0.0',
|
|
550
|
+
'ssn': None,
|
|
551
|
+
'state': 'New South Wales',
|
|
552
|
+
'issueComments': None,
|
|
553
|
+
'timeZone': 'Australia/Sydney',
|
|
554
|
+
'defaultLocale': 'en_US',
|
|
555
|
+
'nationality': None,
|
|
556
|
+
'salaryBudgetLumpsumPercentage': None,
|
|
557
|
+
'sysCostOfSource': None,
|
|
558
|
+
'ethnicity': None,
|
|
559
|
+
'displayName': 'Mark Burke',
|
|
560
|
+
'payGrade': 'GR-06',
|
|
561
|
+
'nickname': None,
|
|
562
|
+
'email': 'Mark.Burke@bestrunsap.com',
|
|
563
|
+
'salaryBudgetExtra2Percentage': None,
|
|
564
|
+
'stockBudgetOther1Amount': None,
|
|
565
|
+
'raiseProrating': None,
|
|
566
|
+
'sysStartingSalary': None,
|
|
567
|
+
'finalJobCode': None,
|
|
568
|
+
'lumpsum2Target': None,
|
|
569
|
+
'stockBudgetOptionAmount': None,
|
|
570
|
+
'country': 'Australia',
|
|
571
|
+
'lastModifiedDateTime': '/Date(1689005658000+0000)/',
|
|
572
|
+
'stockBudgetStockAmount': None,
|
|
573
|
+
'sciLastModified': None,
|
|
574
|
+
'criticalTalentComments': None,
|
|
575
|
+
'homePhone': None,
|
|
576
|
+
'veteranSeparated': False,
|
|
577
|
+
'stockBudgetOther2Amount': None,
|
|
578
|
+
'firstName': 'Mark',
|
|
579
|
+
'stockBudgetUnitAmount': None,
|
|
580
|
+
'salutation': '10808',
|
|
581
|
+
'impactOfLoss': None,
|
|
582
|
+
'benchStrength': None,
|
|
583
|
+
'sysSource': None,
|
|
584
|
+
'futureLeader': None,
|
|
585
|
+
'title': 'HR Business Partner',
|
|
586
|
+
'meritEffectiveDate': None,
|
|
587
|
+
'veteranProtected': False,
|
|
588
|
+
'lumpsumTarget': None,
|
|
589
|
+
'employeeClass': 'Active',
|
|
590
|
+
'hireDate': '/Date(1388534400000)/',
|
|
591
|
+
'matrix2Label': None, 'salaryLocal': None,
|
|
592
|
+
'citizenship': None,
|
|
593
|
+
'reasonForLeaving': None,
|
|
594
|
+
'riskOfLoss': None,
|
|
595
|
+
'location': 'Sydney (8510-0001)',
|
|
596
|
+
'reloComments': None,
|
|
597
|
+
'username': 'mburke',
|
|
598
|
+
'serviceDate': None,
|
|
599
|
+
'reviewFreq': None,
|
|
600
|
+
'salaryBudgetTotalRaisePercentage': None,
|
|
601
|
+
...
|
|
602
|
+
}
|
|
603
|
+
"""
|
|
604
|
+
|
|
605
|
+
if not self._access_token:
|
|
606
|
+
self.authenticate()
|
|
607
|
+
|
|
608
|
+
request_url = self.config()["asUrl"] + "User"
|
|
609
|
+
if user_id:
|
|
610
|
+
# querying a user by key predicate:
|
|
611
|
+
request_url += "('{}')".format(user_id)
|
|
612
|
+
|
|
613
|
+
# Add query parameters (these are NOT passed via JSon body!)
|
|
614
|
+
query = {}
|
|
615
|
+
if field_name and field_value:
|
|
616
|
+
query["$filter"] = "{} {} {}".format(
|
|
617
|
+
field_name, field_operation, field_value
|
|
618
|
+
)
|
|
619
|
+
if max_results > 0:
|
|
620
|
+
query["$top"] = max_results
|
|
621
|
+
encoded_query = urllib.parse.urlencode(query, doseq=True)
|
|
622
|
+
if query:
|
|
623
|
+
request_url += "?" + encoded_query
|
|
624
|
+
|
|
625
|
+
request_header = self.request_header()
|
|
626
|
+
|
|
627
|
+
response = requests.get(
|
|
628
|
+
request_url, headers=request_header, timeout=REQUEST_TIMEOUT
|
|
629
|
+
)
|
|
630
|
+
if response.status_code == 200:
|
|
631
|
+
return self.parse_request_response(response)
|
|
632
|
+
else:
|
|
633
|
+
logger.error(
|
|
634
|
+
"Failed to retrieve user data; status -> %s; error -> %s",
|
|
635
|
+
response.status_code,
|
|
636
|
+
response.text,
|
|
637
|
+
)
|
|
638
|
+
return None
|
|
639
|
+
|
|
640
|
+
# end method definition
|
|
641
|
+
|
|
642
|
+
def get_user_account(self, username: str) -> dict | None:
|
|
643
|
+
"""Get information for a SuccessFactors User Account
|
|
644
|
+
Inactive users are not returned by default. To query inactive users,
|
|
645
|
+
you can explicitly include the status in a $filter or use a key predicate.
|
|
646
|
+
If you want to query all users, use query option $filter=status in 't','f','T','F','e','d'.
|
|
647
|
+
|
|
648
|
+
Args:
|
|
649
|
+
username (str): login name of the user (e.g. "twalker")
|
|
650
|
+
|
|
651
|
+
Returns:
|
|
652
|
+
dict | None: User Account details
|
|
653
|
+
|
|
654
|
+
Example return data in "d" dictionary:
|
|
655
|
+
|
|
656
|
+
{
|
|
657
|
+
'__metadata': {
|
|
658
|
+
'uri': "https://apisalesdemo2.successfactors.eu/odata/v2/UserAccount('twalker')",
|
|
659
|
+
'type': 'SFOData.UserAccount'
|
|
660
|
+
},
|
|
661
|
+
'username': 'twalker',
|
|
662
|
+
'lastModifiedDateTime': '/Date(1692701804000+0000)/',
|
|
663
|
+
'accountUuid': '5c7390e0-d9d2-e348-1700-2b02b3a61aa5',
|
|
664
|
+
'createdDateTime': '/Date(1420745485000+0000)/',
|
|
665
|
+
'timeZone': 'US/Eastern',
|
|
666
|
+
'lastInactivationDateTime': None,
|
|
667
|
+
'accountIsLocked': 'FALSE',
|
|
668
|
+
'accountStatus': 'ACTIVE',
|
|
669
|
+
'defaultLocale': 'en_US',
|
|
670
|
+
'lastLoginFailedDateTime': None,
|
|
671
|
+
'accountId': '90',
|
|
672
|
+
'sapGlobalUserId': None,
|
|
673
|
+
'personIdExternal': '82094',
|
|
674
|
+
'userType': 'employee',
|
|
675
|
+
'email': 'twalker@m365x41497014.onmicrosoft.com',
|
|
676
|
+
'user': {'__deferred': {...}}
|
|
677
|
+
}
|
|
678
|
+
"""
|
|
679
|
+
|
|
680
|
+
if not self._access_token:
|
|
681
|
+
self.authenticate()
|
|
682
|
+
|
|
683
|
+
request_url = self.config()["asUrl"] + "UserAccount('{}')".format(username)
|
|
684
|
+
|
|
685
|
+
request_header = self.request_header()
|
|
686
|
+
|
|
687
|
+
retries = 0
|
|
688
|
+
|
|
689
|
+
while True:
|
|
690
|
+
try:
|
|
691
|
+
response = requests.get(
|
|
692
|
+
request_url, headers=request_header, timeout=REQUEST_TIMEOUT
|
|
693
|
+
)
|
|
694
|
+
response.raise_for_status() # This will raise an HTTPError for bad responses
|
|
695
|
+
return self.parse_request_response(response)
|
|
696
|
+
except requests.exceptions.HTTPError as http_err:
|
|
697
|
+
logger.error(
|
|
698
|
+
"Failed to retrieve user data from SuccessFactors; status -> %s; error -> %s",
|
|
699
|
+
response.status_code,
|
|
700
|
+
str(http_err),
|
|
701
|
+
)
|
|
702
|
+
except requests.exceptions.Timeout:
|
|
703
|
+
logger.warning(
|
|
704
|
+
"Failed to retrieve user data from SuccessFactors. The request timed out.",
|
|
705
|
+
)
|
|
706
|
+
except requests.exceptions.ConnectionError as conn_err:
|
|
707
|
+
logger.error(
|
|
708
|
+
"Cannot connect to SuccessFactors to retrieve user data; status -> %s; error -> %s",
|
|
709
|
+
response.status_code,
|
|
710
|
+
str(conn_err),
|
|
711
|
+
)
|
|
712
|
+
except requests.exceptions.RequestException as req_err:
|
|
713
|
+
logger.error(
|
|
714
|
+
"Failed to retrieve user data from SuccessFactors; status -> %s; error -> %s",
|
|
715
|
+
response.status_code,
|
|
716
|
+
str(req_err),
|
|
717
|
+
)
|
|
718
|
+
retries += 1
|
|
719
|
+
if retries <= REQUEST_MAX_RETRIES:
|
|
720
|
+
logger.info("Retrying in %s seconds...", str(REQUEST_RETRY_DELAY))
|
|
721
|
+
time.sleep(retries * REQUEST_RETRY_DELAY)
|
|
722
|
+
else:
|
|
723
|
+
break
|
|
724
|
+
|
|
725
|
+
return None
|
|
726
|
+
|
|
727
|
+
# end method definition
|
|
728
|
+
|
|
729
|
+
def update_user(
|
|
730
|
+
self,
|
|
731
|
+
user_id: str, # this is NOT the username but really an ID like 106020
|
|
732
|
+
update_data: dict,
|
|
733
|
+
) -> dict:
|
|
734
|
+
"""Update user data. E.g. update the user password or email.
|
|
735
|
+
See: https://help.sap.com/docs/SAP_SUCCESSFACTORS_PLATFORM/d599f15995d348a1b45ba5603e2aba9b/47c39724e7654b99a6be2f71fce1c50b.html?locale=en-US
|
|
736
|
+
|
|
737
|
+
Args:
|
|
738
|
+
user_id (str): ID of the user (e.g. 106020)
|
|
739
|
+
update_data (dict): Update data
|
|
740
|
+
Returns:
|
|
741
|
+
dict: Request response or None if an error occured.
|
|
742
|
+
"""
|
|
743
|
+
|
|
744
|
+
if not self._access_token:
|
|
745
|
+
self.authenticate()
|
|
746
|
+
|
|
747
|
+
request_url = self.config()["asUrl"] + "User('{}')".format(user_id)
|
|
748
|
+
|
|
749
|
+
request_header = self.request_header()
|
|
750
|
+
# We need to use a special MERGE header to tell
|
|
751
|
+
# SuccessFactors to only change the new / provided fields:
|
|
752
|
+
request_header["X-HTTP-METHOD"] = "MERGE"
|
|
753
|
+
|
|
754
|
+
response = requests.post(
|
|
755
|
+
request_url,
|
|
756
|
+
headers=request_header,
|
|
757
|
+
json=update_data,
|
|
758
|
+
timeout=REQUEST_TIMEOUT,
|
|
759
|
+
)
|
|
760
|
+
if response.ok:
|
|
761
|
+
logger.debug("User with ID -> %s updated successfully.", user_id)
|
|
762
|
+
return self.parse_request_response(response)
|
|
763
|
+
else:
|
|
764
|
+
logger.error(
|
|
765
|
+
"Failed to update user with ID -> %s; status -> %s; error -> %s",
|
|
766
|
+
user_id,
|
|
767
|
+
response.status_code,
|
|
768
|
+
response.text,
|
|
769
|
+
)
|
|
770
|
+
return None
|
|
771
|
+
|
|
772
|
+
# end method definition
|
|
773
|
+
|
|
774
|
+
def get_employee(
|
|
775
|
+
self,
|
|
776
|
+
entity: str = "PerPerson",
|
|
777
|
+
field_name: str = "",
|
|
778
|
+
field_value: str = "",
|
|
779
|
+
field_operation: str = "eq",
|
|
780
|
+
max_results: int = 1,
|
|
781
|
+
) -> dict | None:
|
|
782
|
+
"""Get a list of employee(s) matching given criterias.
|
|
783
|
+
|
|
784
|
+
Args:
|
|
785
|
+
entity (str, optional): Entity type to query. Examples are "PerPerson" (default),
|
|
786
|
+
"PerPersonal", "PerEmail", "PersonKey", ...
|
|
787
|
+
field_name (str): Field to search in. E.g. personIdExternal, firstName, lastName,
|
|
788
|
+
fullName, email, dateOfBirth, gender, nationality, maritalStatus,
|
|
789
|
+
employeeId
|
|
790
|
+
field_value (str): Value to match in the Field
|
|
791
|
+
|
|
792
|
+
Returns:
|
|
793
|
+
dict | None: Dictionary with the SuccessFactors object data or None in case the request failed.
|
|
794
|
+
|
|
795
|
+
Example result values for "PerPerson" inside the "d" structure:
|
|
796
|
+
|
|
797
|
+
"results": [
|
|
798
|
+
{
|
|
799
|
+
'__metadata': {...},
|
|
800
|
+
'personIdExternal': '109031',
|
|
801
|
+
'lastModifiedDateTime': '/Date(1442346839000+0000)/',
|
|
802
|
+
'lastModifiedBy': 'admindlr',
|
|
803
|
+
'createdDateTime': '/Date(1442346265000+0000)/',
|
|
804
|
+
'dateOfBirth': '/Date(-501206400000)/',
|
|
805
|
+
'perPersonUuid': '0378B0E6F41444EBB90345B56D537D3D',
|
|
806
|
+
'createdOn': '/Date(1442353465000)/',
|
|
807
|
+
'lastModifiedOn': '/Date(1442354039000)/',
|
|
808
|
+
'countryOfBirth': 'RUS',
|
|
809
|
+
'createdBy': 'admindlr',
|
|
810
|
+
'regionOfBirth': None,
|
|
811
|
+
'personId': '771',
|
|
812
|
+
'personalInfoNav': {...},
|
|
813
|
+
'emergencyContactNav': {...},
|
|
814
|
+
'secondaryAssignmentsNav': {...},
|
|
815
|
+
'personEmpTerminationInfoNav': {...},
|
|
816
|
+
'phoneNav': {...},
|
|
817
|
+
'employmentNav': {...},
|
|
818
|
+
...
|
|
819
|
+
}
|
|
820
|
+
]
|
|
821
|
+
|
|
822
|
+
Example result values for "PerPersonal" inside the "d" structure:
|
|
823
|
+
|
|
824
|
+
"results": [
|
|
825
|
+
{
|
|
826
|
+
'__metadata': {
|
|
827
|
+
'uri': "https://apisalesdemo2.successfactors.eu/odata/v2/PerPersonal(personIdExternal='108729',startDate=datetime'2017-03-13T00:00:00')",
|
|
828
|
+
'type': 'SFOData.PerPersonal'
|
|
829
|
+
},
|
|
830
|
+
'personIdExternal': '108729',
|
|
831
|
+
'startDate': '/Date(1489363200000)/',
|
|
832
|
+
'lastModifiedDateTime': '/Date(1489442337000+0000)/',
|
|
833
|
+
'endDate': '/Date(253402214400000)/',
|
|
834
|
+
'createdDateTime': '/Date(1489442337000+0000)/',
|
|
835
|
+
'suffix': None,
|
|
836
|
+
'attachmentId': None,
|
|
837
|
+
'preferredName': 'Hillary',
|
|
838
|
+
'lastNameAlt1': None,
|
|
839
|
+
'firstName': 'Hillary',
|
|
840
|
+
'nationality': 'USA',
|
|
841
|
+
'salutation': '30085',
|
|
842
|
+
'maritalStatus': '10825',
|
|
843
|
+
'lastName': 'Lawson',
|
|
844
|
+
'gender': 'F',
|
|
845
|
+
'firstNameAlt1': None,
|
|
846
|
+
'createdOn': '/Date(1489445937000)/',
|
|
847
|
+
'middleNameAlt1': None,
|
|
848
|
+
'lastModifiedBy': '82094',
|
|
849
|
+
'lastModifiedOn': '/Date(1489445937000)/',
|
|
850
|
+
'createdBy': '82094',
|
|
851
|
+
'middleName': None,
|
|
852
|
+
'nativePreferredLang': '10249',
|
|
853
|
+
'localNavAUS': {'__deferred': {...}},
|
|
854
|
+
'localNavBGD': {'__deferred': {...}},
|
|
855
|
+
'localNavHKG': {'__deferred': {...}},
|
|
856
|
+
'localNavMYS': {'__deferred': {...}},
|
|
857
|
+
'localNavAUT': {'__deferred': {...}},
|
|
858
|
+
'localNavLKA': {'__deferred': {...}},
|
|
859
|
+
'localNavPOL': {'__deferred': {...}},
|
|
860
|
+
'localNavCZE': {'__deferred': {...}},
|
|
861
|
+
'localNavTWN': {'__deferred': {...}},
|
|
862
|
+
'localNavARE': {'__deferred': {...}},
|
|
863
|
+
'localNavARG': {'__deferred': {...}},
|
|
864
|
+
'localNavCAN': {'__deferred': {...}},
|
|
865
|
+
'localNavNOR': {'__deferred': {...}},
|
|
866
|
+
'localNavOMN': {'__deferred': {...}},
|
|
867
|
+
'localNavPER': {'__deferred': {...}},
|
|
868
|
+
'localNavSGP': {'__deferred': {...}},
|
|
869
|
+
'localNavVEN': {'__deferred': {...}},
|
|
870
|
+
'localNavZAF': {'__deferred': {...}},
|
|
871
|
+
'localNavCHL': {'__deferred': {...}},
|
|
872
|
+
'localNavCHE': {'__deferred': {...}},
|
|
873
|
+
'localNavDNK': {'__deferred': {...}},
|
|
874
|
+
'localNavGTM': {'__deferred': {...}},
|
|
875
|
+
'localNavNZL': {'__deferred': {...}},
|
|
876
|
+
'salutationNav': {'__deferred': {...}},
|
|
877
|
+
'localNavCHN': {'__deferred': {...}},
|
|
878
|
+
'localNavVNM': {'__deferred': {...}},
|
|
879
|
+
'localNavIDN': {'__deferred': {...}},
|
|
880
|
+
'localNavPRT': {'__deferred': {...}},
|
|
881
|
+
'localNavCOL': {'__deferred': {...}},
|
|
882
|
+
'localNavHUN': {'__deferred': {...}},
|
|
883
|
+
'localNavSWE': {'__deferred': {...}},
|
|
884
|
+
'localNavESP': {'__deferred': {...}},
|
|
885
|
+
'localNavUSA': {'__deferred': {...}},
|
|
886
|
+
'nativePreferredLangNav': {'__deferred': {...}},
|
|
887
|
+
'maritalStatusNav': {'__deferred': {...}}, ...}
|
|
888
|
+
"""
|
|
889
|
+
|
|
890
|
+
if not self._access_token:
|
|
891
|
+
self.authenticate()
|
|
892
|
+
|
|
893
|
+
# Add query parameters (these are NOT passed via JSon body!)
|
|
894
|
+
query = {}
|
|
895
|
+
if field_name and field_value:
|
|
896
|
+
query["$filter"] = "{} {} {}".format(
|
|
897
|
+
field_name, field_operation, field_value
|
|
898
|
+
)
|
|
899
|
+
if max_results > 0:
|
|
900
|
+
query["$top"] = max_results
|
|
901
|
+
encoded_query = urllib.parse.urlencode(query, doseq=True)
|
|
902
|
+
|
|
903
|
+
request_url = self.config()["asUrl"] + entity
|
|
904
|
+
if query:
|
|
905
|
+
request_url += "?" + encoded_query
|
|
906
|
+
|
|
907
|
+
request_header = self.request_header()
|
|
908
|
+
|
|
909
|
+
response = requests.get(
|
|
910
|
+
request_url, headers=request_header, timeout=REQUEST_TIMEOUT
|
|
911
|
+
)
|
|
912
|
+
if response.status_code == 200:
|
|
913
|
+
return self.parse_request_response(response)
|
|
914
|
+
else:
|
|
915
|
+
logger.error(
|
|
916
|
+
"Failed to retrieve employee data; status -> %s; error -> %s",
|
|
917
|
+
response.status_code,
|
|
918
|
+
response.text,
|
|
919
|
+
)
|
|
920
|
+
return None
|
|
921
|
+
|
|
922
|
+
# end method definition
|
|
923
|
+
|
|
924
|
+
def get_entities_metadata(self, entities: list | None = None) -> dict | None:
|
|
925
|
+
"""Get the schema (metadata) for a list of entities (list can be empty to get it for all)
|
|
926
|
+
IMPORTANT: A metadata request using $metadata returns an XML serialization of the service,
|
|
927
|
+
including the entity data model (EDM) and the service operation descriptions.
|
|
928
|
+
The metadata response supports only application/xml type.
|
|
929
|
+
|
|
930
|
+
Args:
|
|
931
|
+
entities (list): list of entities to deliver metadata for
|
|
932
|
+
|
|
933
|
+
Returns:
|
|
934
|
+
dict | None: Dictionary with the SuccessFactors object data or None in case the request failed.
|
|
935
|
+
"""
|
|
936
|
+
|
|
937
|
+
if not self._access_token:
|
|
938
|
+
self.authenticate()
|
|
939
|
+
|
|
940
|
+
request_url = self.config()["asUrl"]
|
|
941
|
+
if entities:
|
|
942
|
+
request_url += "{}/".format(",".join(entities))
|
|
943
|
+
request_url += "$metadata"
|
|
944
|
+
|
|
945
|
+
request_header = self.request_header()
|
|
946
|
+
request_header["Accept"] = "application/xml"
|
|
947
|
+
|
|
948
|
+
response = requests.get(
|
|
949
|
+
request_url, headers=request_header, timeout=REQUEST_TIMEOUT
|
|
950
|
+
)
|
|
951
|
+
if response.status_code == 200:
|
|
952
|
+
return xmltodict.parse(response.text)
|
|
953
|
+
else:
|
|
954
|
+
logger.error(
|
|
955
|
+
"Failed to retrieve entity data; status -> %s; error -> %s",
|
|
956
|
+
response.status_code,
|
|
957
|
+
response.text,
|
|
958
|
+
)
|
|
959
|
+
return None
|
|
960
|
+
|
|
961
|
+
# end method definition
|
|
962
|
+
|
|
963
|
+
def get_entity_metadata(self, entity: str) -> dict | None:
|
|
964
|
+
"""Get the schema (metadata) for an entity
|
|
965
|
+
|
|
966
|
+
Args:
|
|
967
|
+
entity (str): entity to deliver metadata for
|
|
968
|
+
|
|
969
|
+
Returns:
|
|
970
|
+
dict | None: Dictionary with the SuccessFactors object data or None in case the request failed.
|
|
971
|
+
"""
|
|
972
|
+
|
|
973
|
+
if not self._access_token:
|
|
974
|
+
self.authenticate()
|
|
975
|
+
|
|
976
|
+
if not entity:
|
|
977
|
+
return None
|
|
978
|
+
|
|
979
|
+
request_url = self.config()["asUrl"] + "Entity('{}')?$format=JSON".format(
|
|
980
|
+
entity
|
|
981
|
+
)
|
|
982
|
+
|
|
983
|
+
request_header = self.request_header()
|
|
984
|
+
|
|
985
|
+
response = requests.get(
|
|
986
|
+
request_url, headers=request_header, timeout=REQUEST_TIMEOUT
|
|
987
|
+
)
|
|
988
|
+
if response.status_code == 200:
|
|
989
|
+
return self.parse_request_response(response)
|
|
990
|
+
else:
|
|
991
|
+
logger.error(
|
|
992
|
+
"Failed to retrieve entity data; status -> %s; error -> %s",
|
|
993
|
+
response.status_code,
|
|
994
|
+
response.text,
|
|
995
|
+
)
|
|
996
|
+
return None
|
|
997
|
+
|
|
998
|
+
# end method definition
|
|
999
|
+
|
|
1000
|
+
def update_user_email(
|
|
1001
|
+
self,
|
|
1002
|
+
user_id: str, # this is NOT the username but really an ID like 106020
|
|
1003
|
+
email_address: str,
|
|
1004
|
+
email_type: int = 8448, # 8448
|
|
1005
|
+
) -> dict:
|
|
1006
|
+
"""Update user email.
|
|
1007
|
+
See: https://help.sap.com/docs/SAP_SUCCESSFACTORS_PLATFORM/d599f15995d348a1b45ba5603e2aba9b/7b3daeb3d77d491bb401345eede34bb5.html?locale=en-US
|
|
1008
|
+
|
|
1009
|
+
Args:
|
|
1010
|
+
user_id (str): ID of the user (e.g. 106020)
|
|
1011
|
+
email_address (str): new email address of user
|
|
1012
|
+
email_type (int): Type of the email. 8448 = Business
|
|
1013
|
+
Returns:
|
|
1014
|
+
dict: Request response or None if an error occured.
|
|
1015
|
+
"""
|
|
1016
|
+
|
|
1017
|
+
if not self._access_token:
|
|
1018
|
+
self.authenticate()
|
|
1019
|
+
|
|
1020
|
+
request_url = self.config()["asUrl"] + "upsert"
|
|
1021
|
+
|
|
1022
|
+
update_data = {
|
|
1023
|
+
"__metadata": {
|
|
1024
|
+
"uri": "PerEmail(emailType='{}',personIdExternal='{}')".format(
|
|
1025
|
+
email_type, user_id
|
|
1026
|
+
),
|
|
1027
|
+
"type": "SFOData.PerEmail",
|
|
1028
|
+
},
|
|
1029
|
+
"emailAddress": email_address,
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
request_header = self.request_header()
|
|
1033
|
+
|
|
1034
|
+
response = requests.post(
|
|
1035
|
+
request_url,
|
|
1036
|
+
headers=request_header,
|
|
1037
|
+
json=update_data,
|
|
1038
|
+
timeout=REQUEST_TIMEOUT,
|
|
1039
|
+
)
|
|
1040
|
+
if response.ok:
|
|
1041
|
+
logger.debug(
|
|
1042
|
+
"Email of user with ID -> %s successfully updated to -> %s.",
|
|
1043
|
+
user_id,
|
|
1044
|
+
email_address,
|
|
1045
|
+
)
|
|
1046
|
+
return self.parse_request_response(response)
|
|
1047
|
+
else:
|
|
1048
|
+
logger.error(
|
|
1049
|
+
"Failed to set email of user with ID -> %s; status -> %s; error -> %s",
|
|
1050
|
+
user_id,
|
|
1051
|
+
response.status_code,
|
|
1052
|
+
response.text,
|
|
1053
|
+
)
|
|
1054
|
+
return None
|
|
1055
|
+
|
|
1056
|
+
# end method definition
|