pyxecm 1.3.0__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 +6 -0
- pyxecm/customizer/browser_automation.py +231 -56
- pyxecm/customizer/customizer.py +466 -235
- pyxecm/customizer/k8s.py +49 -27
- pyxecm/customizer/m365.py +1183 -263
- pyxecm/customizer/payload.py +13854 -5368
- pyxecm/customizer/pht.py +503 -0
- pyxecm/customizer/salesforce.py +1782 -0
- pyxecm/customizer/sap.py +5 -5
- pyxecm/customizer/servicenow.py +1221 -0
- pyxecm/customizer/successfactors.py +1056 -0
- pyxecm/customizer/translate.py +2 -2
- pyxecm/helper/__init__.py +2 -0
- pyxecm/helper/assoc.py +27 -7
- pyxecm/helper/data.py +1527 -0
- pyxecm/helper/web.py +189 -25
- pyxecm/helper/xml.py +244 -40
- pyxecm/otac.py +311 -25
- pyxecm/otcs.py +3866 -1103
- pyxecm/otds.py +397 -150
- pyxecm/otiv.py +1 -1
- pyxecm/otmm.py +808 -0
- pyxecm/otpd.py +17 -12
- {pyxecm-1.3.0.dist-info → pyxecm-1.5.dist-info}/METADATA +4 -1
- pyxecm-1.5.dist-info/RECORD +30 -0
- {pyxecm-1.3.0.dist-info → pyxecm-1.5.dist-info}/WHEEL +1 -1
- pyxecm-1.3.0.dist-info/RECORD +0 -23
- {pyxecm-1.3.0.dist-info → pyxecm-1.5.dist-info}/LICENSE +0 -0
- {pyxecm-1.3.0.dist-info → pyxecm-1.5.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,1782 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Salesforce Module to interact with the Salesforce API
|
|
3
|
+
See: https://developer.salesforce.com/docs/atlas.en-us.api_rest.meta/api_rest/intro_rest.htm
|
|
4
|
+
|
|
5
|
+
Class: Salesforce
|
|
6
|
+
Methods:
|
|
7
|
+
|
|
8
|
+
__init__ : class initializer
|
|
9
|
+
config : Returns config data set
|
|
10
|
+
credentials: Returns the token data
|
|
11
|
+
request_header: Returns the request header for Salesforce API calls
|
|
12
|
+
parse_request_response: Parse the REST API responses and convert
|
|
13
|
+
them to Python dict in a safe way
|
|
14
|
+
exist_result_item: Check if an dict item is in the response
|
|
15
|
+
of the Salesforce API call
|
|
16
|
+
get_result_value: Check if a defined value (based on a key) is in the Salesforce API response
|
|
17
|
+
|
|
18
|
+
authenticate: Authenticates at Salesforce API
|
|
19
|
+
|
|
20
|
+
get_object_id_by_name: Get the ID of a given Salesforce object with a given type and name
|
|
21
|
+
get_object: Get a Salesforce object based on a defined
|
|
22
|
+
field value and return selected result fields.
|
|
23
|
+
add_object: Add object to Salesforce. This is a generic wrapper method
|
|
24
|
+
for the actual add methods.
|
|
25
|
+
|
|
26
|
+
get_group: Get a Salesforce group based on its ID.
|
|
27
|
+
add_group: Add a new Salesforce group.
|
|
28
|
+
update_group: Update a Salesforce group.
|
|
29
|
+
get_group_members: Get Salesforce group members
|
|
30
|
+
add_group_member: Add a user or group to a Salesforce group
|
|
31
|
+
|
|
32
|
+
get_all_user_profiles: Get all user profiles
|
|
33
|
+
get_user_profile_id: Get a user profile ID by profile name
|
|
34
|
+
get_user_id: Get a user ID by user name
|
|
35
|
+
get_user: Get a Salesforce user based on its ID.
|
|
36
|
+
add_user: Add a new Salesforce user.
|
|
37
|
+
update_user: Update a Salesforce user.
|
|
38
|
+
update_user_password: Update the password of a Salesforce user.
|
|
39
|
+
update_user_photo: update the Salesforce user photo.
|
|
40
|
+
|
|
41
|
+
add_account: Add a new Account object to Salesforce.
|
|
42
|
+
add_product: Add a new Product object to Salesforce.
|
|
43
|
+
add_opportunity: Add a new Opportunity object to Salesfoce.
|
|
44
|
+
add_case: Add a new Case object to Salesforce. The case number
|
|
45
|
+
is automatically created and can not be provided.
|
|
46
|
+
add_asset: Add a new Asset object to Salesforce.
|
|
47
|
+
add_contract: Add a new Contract object to Salesforce.
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
__author__ = "Dr. Marc Diefenbruch"
|
|
51
|
+
__copyright__ = "Copyright 2024, OpenText"
|
|
52
|
+
__credits__ = ["Kai-Philip Gatzweiler"]
|
|
53
|
+
__maintainer__ = "Dr. Marc Diefenbruch"
|
|
54
|
+
__email__ = "mdiefenb@opentext.com"
|
|
55
|
+
|
|
56
|
+
import os
|
|
57
|
+
import json
|
|
58
|
+
import logging
|
|
59
|
+
|
|
60
|
+
from typing import Optional, Union, Any
|
|
61
|
+
import requests
|
|
62
|
+
|
|
63
|
+
logger = logging.getLogger("pyxecm.customizer.salesforce")
|
|
64
|
+
|
|
65
|
+
REQUEST_LOGIN_HEADERS = {
|
|
66
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
67
|
+
"Accept": "application/json",
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
REQUEST_TIMEOUT = 60
|
|
71
|
+
SALESFORCE_API_VERSION = "v60.0"
|
|
72
|
+
|
|
73
|
+
class Salesforce(object):
|
|
74
|
+
"""Used to retrieve and automate stettings in Salesforce."""
|
|
75
|
+
|
|
76
|
+
_config: dict
|
|
77
|
+
_access_token = None
|
|
78
|
+
_instance_url = None
|
|
79
|
+
|
|
80
|
+
def __init__(
|
|
81
|
+
self,
|
|
82
|
+
base_url: str,
|
|
83
|
+
client_id: str,
|
|
84
|
+
client_secret: str,
|
|
85
|
+
username: str,
|
|
86
|
+
password: str,
|
|
87
|
+
authorization_url: str = "",
|
|
88
|
+
security_token: str = "",
|
|
89
|
+
):
|
|
90
|
+
"""Initialize the Salesforce object
|
|
91
|
+
|
|
92
|
+
Args:
|
|
93
|
+
base_url (str): base URL of the Salesforce tenant
|
|
94
|
+
authorization_url (str): authorization URL of the Salesforce tenant, typically ending with "/services/oauth2/token"
|
|
95
|
+
client_id (str): Salesforce Client ID
|
|
96
|
+
client_secret (str): Salesforce Client Secret
|
|
97
|
+
username (str): user name in Saleforce
|
|
98
|
+
password (str): password of the user
|
|
99
|
+
authorization_url (str, optional): URL for Salesforce login. If not given it will be constructed with default values
|
|
100
|
+
using base_url
|
|
101
|
+
security_token (str, optional): security token for Salesforce login
|
|
102
|
+
"""
|
|
103
|
+
|
|
104
|
+
# The instance URL is also returned by the authenticate call
|
|
105
|
+
# but typically it is identical to the base_url.
|
|
106
|
+
self._instance_url = base_url
|
|
107
|
+
|
|
108
|
+
salesforce_config = {}
|
|
109
|
+
|
|
110
|
+
# Store the credentials and parameters in a config dictionary:
|
|
111
|
+
salesforce_config["clientId"] = client_id
|
|
112
|
+
salesforce_config["clientSecret"] = client_secret
|
|
113
|
+
salesforce_config["username"] = username
|
|
114
|
+
salesforce_config["password"] = password
|
|
115
|
+
salesforce_config["securityToken"] = security_token
|
|
116
|
+
|
|
117
|
+
# Set the Salesforce URLs and REST API endpoints:
|
|
118
|
+
salesforce_config["baseUrl"] = base_url
|
|
119
|
+
salesforce_config["objectUrl"] = salesforce_config[
|
|
120
|
+
"baseUrl"
|
|
121
|
+
] + "/services/data/{}/sobjects/".format(SALESFORCE_API_VERSION)
|
|
122
|
+
salesforce_config["queryUrl"] = salesforce_config[
|
|
123
|
+
"baseUrl"
|
|
124
|
+
] + "/services/data/{}/query/".format(SALESFORCE_API_VERSION)
|
|
125
|
+
salesforce_config["compositeUrl"] = salesforce_config[
|
|
126
|
+
"baseUrl"
|
|
127
|
+
] + "/services/data/{}/composite/".format(SALESFORCE_API_VERSION)
|
|
128
|
+
salesforce_config["connectUrl"] = salesforce_config[
|
|
129
|
+
"baseUrl"
|
|
130
|
+
] + "/services/data/{}/connect/".format(SALESFORCE_API_VERSION)
|
|
131
|
+
salesforce_config["toolingUrl"] = salesforce_config[
|
|
132
|
+
"baseUrl"
|
|
133
|
+
] + "/services/data/{}/tooling/".format(SALESFORCE_API_VERSION)
|
|
134
|
+
if authorization_url:
|
|
135
|
+
salesforce_config["authenticationUrl"] = authorization_url
|
|
136
|
+
else:
|
|
137
|
+
salesforce_config["authenticationUrl"] = (
|
|
138
|
+
salesforce_config["baseUrl"] + "/services/oauth2/token"
|
|
139
|
+
)
|
|
140
|
+
# URLs that are based on the objectURL (sobjects/):
|
|
141
|
+
salesforce_config["userUrl"] = salesforce_config["objectUrl"] + "User/"
|
|
142
|
+
salesforce_config["groupUrl"] = salesforce_config["objectUrl"] + "Group/"
|
|
143
|
+
salesforce_config["groupMemberUrl"] = (
|
|
144
|
+
salesforce_config["objectUrl"] + "GroupMember/"
|
|
145
|
+
)
|
|
146
|
+
salesforce_config["accountUrl"] = salesforce_config["objectUrl"] + "Account/"
|
|
147
|
+
salesforce_config["productUrl"] = salesforce_config["objectUrl"] + "Product2/"
|
|
148
|
+
salesforce_config["opportunityUrl"] = (
|
|
149
|
+
salesforce_config["objectUrl"] + "Opportunity/"
|
|
150
|
+
)
|
|
151
|
+
salesforce_config["caseUrl"] = salesforce_config["objectUrl"] + "Case/"
|
|
152
|
+
salesforce_config["assetUrl"] = salesforce_config["objectUrl"] + "Asset/"
|
|
153
|
+
salesforce_config["contractUrl"] = salesforce_config["objectUrl"] + "Contract/"
|
|
154
|
+
|
|
155
|
+
# Set the data for the token request
|
|
156
|
+
salesforce_config["authenticationData"] = {
|
|
157
|
+
"grant_type": "password",
|
|
158
|
+
"client_id": client_id,
|
|
159
|
+
"client_secret": client_secret,
|
|
160
|
+
"username": username,
|
|
161
|
+
"password": password,
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
self._config = salesforce_config
|
|
165
|
+
|
|
166
|
+
# end method definition
|
|
167
|
+
|
|
168
|
+
def config(self) -> dict:
|
|
169
|
+
"""Returns the configuration dictionary
|
|
170
|
+
|
|
171
|
+
Returns:
|
|
172
|
+
dict: Configuration dictionary
|
|
173
|
+
"""
|
|
174
|
+
return self._config
|
|
175
|
+
|
|
176
|
+
# end method definition
|
|
177
|
+
|
|
178
|
+
def credentials(self) -> dict:
|
|
179
|
+
"""Return the login credentials
|
|
180
|
+
|
|
181
|
+
Returns:
|
|
182
|
+
dict: dictionary with login credentials for Salesforce
|
|
183
|
+
"""
|
|
184
|
+
return self.config()["authenticationData"]
|
|
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
|
+
}
|
|
201
|
+
if content_type:
|
|
202
|
+
request_header["Content-Type"] = content_type
|
|
203
|
+
|
|
204
|
+
return request_header
|
|
205
|
+
|
|
206
|
+
# end method definition
|
|
207
|
+
|
|
208
|
+
def parse_request_response(
|
|
209
|
+
self,
|
|
210
|
+
response_object: requests.Response,
|
|
211
|
+
additional_error_message: str = "",
|
|
212
|
+
show_error: bool = True,
|
|
213
|
+
) -> dict | None:
|
|
214
|
+
"""Converts the request response (JSon) to a Python dict in a safe way
|
|
215
|
+
that also handles exceptions. It first tries to load the response.text
|
|
216
|
+
via json.loads() that produces a dict output. Only if response.text is
|
|
217
|
+
not set or is empty it just converts the response_object to a dict using
|
|
218
|
+
the vars() built-in method.
|
|
219
|
+
|
|
220
|
+
Args:
|
|
221
|
+
response_object (object): this is reponse object delivered by the request call
|
|
222
|
+
additional_error_message (str, optional): use a more specific error message
|
|
223
|
+
in case of an error
|
|
224
|
+
show_error (bool): True: write an error to the log file
|
|
225
|
+
False: write a warning to the log file
|
|
226
|
+
Returns:
|
|
227
|
+
dict: response information or None in case of an error
|
|
228
|
+
"""
|
|
229
|
+
|
|
230
|
+
if not response_object:
|
|
231
|
+
return None
|
|
232
|
+
|
|
233
|
+
try:
|
|
234
|
+
if response_object.text:
|
|
235
|
+
dict_object = json.loads(response_object.text)
|
|
236
|
+
else:
|
|
237
|
+
dict_object = vars(response_object)
|
|
238
|
+
except json.JSONDecodeError as exception:
|
|
239
|
+
if additional_error_message:
|
|
240
|
+
message = "Cannot decode response as JSon. {}; error -> {}".format(
|
|
241
|
+
additional_error_message, exception
|
|
242
|
+
)
|
|
243
|
+
else:
|
|
244
|
+
message = "Cannot decode response as JSon; error -> {}".format(
|
|
245
|
+
exception
|
|
246
|
+
)
|
|
247
|
+
if show_error:
|
|
248
|
+
logger.error(message)
|
|
249
|
+
else:
|
|
250
|
+
logger.warning(message)
|
|
251
|
+
return None
|
|
252
|
+
else:
|
|
253
|
+
return dict_object
|
|
254
|
+
|
|
255
|
+
# end method definition
|
|
256
|
+
|
|
257
|
+
def exist_result_item(self, response: dict, key: str, value: str) -> bool:
|
|
258
|
+
"""Check existence of key / value pair in the response properties of an Salesforce API call.
|
|
259
|
+
|
|
260
|
+
Args:
|
|
261
|
+
response (dict): REST response from an Salesforce API call
|
|
262
|
+
key (str): property name (key)
|
|
263
|
+
value (str): value to find in the item with the matching key
|
|
264
|
+
Returns:
|
|
265
|
+
bool: True if the value was found, False otherwise
|
|
266
|
+
"""
|
|
267
|
+
|
|
268
|
+
if not response:
|
|
269
|
+
return False
|
|
270
|
+
|
|
271
|
+
if "records" in response:
|
|
272
|
+
records = response["records"]
|
|
273
|
+
if not records or not isinstance(records, list):
|
|
274
|
+
return False
|
|
275
|
+
|
|
276
|
+
for record in records:
|
|
277
|
+
if value == record[key]:
|
|
278
|
+
return True
|
|
279
|
+
else:
|
|
280
|
+
if not key in response:
|
|
281
|
+
return False
|
|
282
|
+
if value == response[key]:
|
|
283
|
+
return True
|
|
284
|
+
|
|
285
|
+
return False
|
|
286
|
+
|
|
287
|
+
# end method definition
|
|
288
|
+
|
|
289
|
+
def get_result_value(
|
|
290
|
+
self,
|
|
291
|
+
response: dict,
|
|
292
|
+
key: str,
|
|
293
|
+
index: int = 0,
|
|
294
|
+
) -> str | None:
|
|
295
|
+
"""Get value of a result property with a given key of an Salesforce API call.
|
|
296
|
+
|
|
297
|
+
Args:
|
|
298
|
+
response (dict): REST response from an Salesforce REST Call
|
|
299
|
+
key (str): property name (key)
|
|
300
|
+
index (int, optional): Index to use (1st element has index 0).
|
|
301
|
+
Defaults to 0.
|
|
302
|
+
Returns:
|
|
303
|
+
str: value for the key, None otherwise
|
|
304
|
+
"""
|
|
305
|
+
|
|
306
|
+
if not response:
|
|
307
|
+
return None
|
|
308
|
+
|
|
309
|
+
# do we have a complex response - e.g. from an SOQL query?
|
|
310
|
+
# these have list of "records":
|
|
311
|
+
if "records" in response:
|
|
312
|
+
values = response["records"]
|
|
313
|
+
if not values or not isinstance(values, list) or len(values) - 1 < index:
|
|
314
|
+
return None
|
|
315
|
+
value = values[index][key]
|
|
316
|
+
else: # simple response - try to find key in response directly:
|
|
317
|
+
if not key in response:
|
|
318
|
+
return None
|
|
319
|
+
value = response[key]
|
|
320
|
+
|
|
321
|
+
return value
|
|
322
|
+
|
|
323
|
+
# end method definition
|
|
324
|
+
|
|
325
|
+
def authenticate(self, revalidate: bool = False) -> str | None:
|
|
326
|
+
"""Authenticate at Salesforce with client ID and client secret.
|
|
327
|
+
|
|
328
|
+
Args:
|
|
329
|
+
revalidate (bool, optional): determinse if a re-athentication is enforced
|
|
330
|
+
(e.g. if session has timed out with 401 error)
|
|
331
|
+
Returns:
|
|
332
|
+
str: Access token. Also stores access token in self._access_token. None in case of error
|
|
333
|
+
"""
|
|
334
|
+
|
|
335
|
+
# Already authenticated and session still valid?
|
|
336
|
+
if self._access_token and not revalidate:
|
|
337
|
+
logger.debug(
|
|
338
|
+
"Session still valid - return existing access token -> %s",
|
|
339
|
+
str(self._access_token),
|
|
340
|
+
)
|
|
341
|
+
return self._access_token
|
|
342
|
+
|
|
343
|
+
request_url = self.config()["authenticationUrl"]
|
|
344
|
+
request_header = REQUEST_LOGIN_HEADERS
|
|
345
|
+
|
|
346
|
+
logger.debug("Requesting Salesforce Access Token from -> %s", request_url)
|
|
347
|
+
|
|
348
|
+
authenticate_post_body = self.credentials()
|
|
349
|
+
|
|
350
|
+
response = None
|
|
351
|
+
self._access_token = None
|
|
352
|
+
self._instance_url = None
|
|
353
|
+
|
|
354
|
+
try:
|
|
355
|
+
response = requests.post(
|
|
356
|
+
request_url,
|
|
357
|
+
data=authenticate_post_body,
|
|
358
|
+
headers=request_header,
|
|
359
|
+
timeout=REQUEST_TIMEOUT,
|
|
360
|
+
)
|
|
361
|
+
except requests.exceptions.ConnectionError as exception:
|
|
362
|
+
logger.warning(
|
|
363
|
+
"Unable to connect to -> %s : %s",
|
|
364
|
+
self.config()["authenticationUrl"],
|
|
365
|
+
exception,
|
|
366
|
+
)
|
|
367
|
+
return None
|
|
368
|
+
|
|
369
|
+
if response.ok:
|
|
370
|
+
authenticate_dict = self.parse_request_response(response)
|
|
371
|
+
if not authenticate_dict:
|
|
372
|
+
return None
|
|
373
|
+
else:
|
|
374
|
+
# Store authentication access_token:
|
|
375
|
+
self._access_token = authenticate_dict["access_token"]
|
|
376
|
+
logger.debug("Access Token -> %s", self._access_token)
|
|
377
|
+
self._instance_url = authenticate_dict["instance_url"]
|
|
378
|
+
logger.debug("Instance URL -> %s", self._instance_url)
|
|
379
|
+
else:
|
|
380
|
+
logger.error(
|
|
381
|
+
"Failed to request an Salesforce Access Token; error -> %s",
|
|
382
|
+
response.text,
|
|
383
|
+
)
|
|
384
|
+
return None
|
|
385
|
+
|
|
386
|
+
return self._access_token
|
|
387
|
+
|
|
388
|
+
# end method definition
|
|
389
|
+
|
|
390
|
+
def get_object_id_by_name(
|
|
391
|
+
self, object_type: str, name: str, name_field: str = "Name"
|
|
392
|
+
) -> Optional[str]:
|
|
393
|
+
"""Get the ID of a given Salesforce object with a given type and name.
|
|
394
|
+
|
|
395
|
+
Args:
|
|
396
|
+
object_type (str): Sales object type, like "Account", "Case", ...
|
|
397
|
+
name (str): Name of the Salesforce object.
|
|
398
|
+
name_field (str, optional): Field where the name is stored. Defaults to "Name".
|
|
399
|
+
|
|
400
|
+
Returns:
|
|
401
|
+
Optional[str]: Object ID or None if the request fails.
|
|
402
|
+
"""
|
|
403
|
+
|
|
404
|
+
if not self._access_token or not self._instance_url:
|
|
405
|
+
self.authenticate()
|
|
406
|
+
|
|
407
|
+
request_header = self.request_header()
|
|
408
|
+
request_url = self.config()["queryUrl"]
|
|
409
|
+
|
|
410
|
+
query = f"SELECT Id FROM {object_type} WHERE {name_field} = '{name}'"
|
|
411
|
+
|
|
412
|
+
retries = 0
|
|
413
|
+
while True:
|
|
414
|
+
response = requests.get(
|
|
415
|
+
url=request_url,
|
|
416
|
+
headers=request_header,
|
|
417
|
+
params={"q": query},
|
|
418
|
+
timeout=REQUEST_TIMEOUT,
|
|
419
|
+
)
|
|
420
|
+
if response.ok:
|
|
421
|
+
response = self.parse_request_response(response)
|
|
422
|
+
object_id = self.get_result_value(response, "Id")
|
|
423
|
+
return object_id
|
|
424
|
+
elif response.status_code == 401 and retries == 0:
|
|
425
|
+
logger.debug("Session has expired - try to re-authenticate...")
|
|
426
|
+
self.authenticate(revalidate=True)
|
|
427
|
+
request_header = self.request_header()
|
|
428
|
+
retries += 1
|
|
429
|
+
else:
|
|
430
|
+
logger.error(
|
|
431
|
+
"Failed to get Salesforce object ID for object type -> '%s' and object name -> '%s'; status -> %s; error -> %s",
|
|
432
|
+
object_type,
|
|
433
|
+
name,
|
|
434
|
+
response.status_code,
|
|
435
|
+
response.text,
|
|
436
|
+
)
|
|
437
|
+
return None
|
|
438
|
+
|
|
439
|
+
# end method definition
|
|
440
|
+
|
|
441
|
+
def get_object(
|
|
442
|
+
self,
|
|
443
|
+
object_type: str,
|
|
444
|
+
search_field: str,
|
|
445
|
+
search_value: str,
|
|
446
|
+
result_fields: list | None,
|
|
447
|
+
limit: int = 200,
|
|
448
|
+
) -> dict | None:
|
|
449
|
+
"""Get a Salesforce object based on a defined field value and return selected result fields.
|
|
450
|
+
|
|
451
|
+
Args:
|
|
452
|
+
object_type (str): Salesforce Business Object type. Such as "Account" or "Case".
|
|
453
|
+
search_field (str): object field to search in
|
|
454
|
+
search_value (str): value to search for
|
|
455
|
+
result_fields (list | None): list of fields to return. If None, then all standard fields
|
|
456
|
+
of the object will be returned.
|
|
457
|
+
limit (int, optional): maximum number of fields to return. Salesforce enforces 200 as upper limit.
|
|
458
|
+
|
|
459
|
+
Returns:
|
|
460
|
+
dict | None: Dictionary with the Salesforce object data.
|
|
461
|
+
|
|
462
|
+
Example response:
|
|
463
|
+
{
|
|
464
|
+
'totalSize': 2,
|
|
465
|
+
'done': True,
|
|
466
|
+
'records': [
|
|
467
|
+
{
|
|
468
|
+
'attributes': {
|
|
469
|
+
'type': 'Opportunity',
|
|
470
|
+
'url': '/services/data/v60.0/sobjects/Opportunity/006Dn00000EclybIAB'
|
|
471
|
+
},
|
|
472
|
+
'Id': '006Dn00000EclybIAB'
|
|
473
|
+
},
|
|
474
|
+
{
|
|
475
|
+
'attributes': {
|
|
476
|
+
'type': 'Opportunity',
|
|
477
|
+
'url': '/services/data/v60.0/sobjects/Opportunity/006Dn00000EclyfIAB'
|
|
478
|
+
},
|
|
479
|
+
'Id': '006Dn00000EclyfIAB'
|
|
480
|
+
}
|
|
481
|
+
]
|
|
482
|
+
}
|
|
483
|
+
"""
|
|
484
|
+
|
|
485
|
+
if not self._access_token or not self._instance_url:
|
|
486
|
+
self.authenticate()
|
|
487
|
+
|
|
488
|
+
if search_field and not search_value:
|
|
489
|
+
logger.error(
|
|
490
|
+
"No search value has been provided for search field -> %s!",
|
|
491
|
+
search_field,
|
|
492
|
+
)
|
|
493
|
+
return None
|
|
494
|
+
if not result_fields:
|
|
495
|
+
logger.debug(
|
|
496
|
+
"No result fields defined. Using 'FIELDS(STANDARD)' to deliver all standard fields of the object."
|
|
497
|
+
)
|
|
498
|
+
result_fields = ["FIELDS(STANDARD)"]
|
|
499
|
+
|
|
500
|
+
query = "SELECT {} FROM {}".format(", ".join(result_fields), object_type)
|
|
501
|
+
if search_field and search_value:
|
|
502
|
+
query += " WHERE {}='{}'".format(search_field, search_value)
|
|
503
|
+
query += " LIMIT {}".format(str(limit))
|
|
504
|
+
|
|
505
|
+
request_header = self.request_header()
|
|
506
|
+
request_url = self.config()["queryUrl"] + "?q={}".format(query)
|
|
507
|
+
|
|
508
|
+
logger.debug(
|
|
509
|
+
"Sending query -> %s to Salesforce; calling -> %s", query, request_url
|
|
510
|
+
)
|
|
511
|
+
|
|
512
|
+
retries = 0
|
|
513
|
+
while True:
|
|
514
|
+
response = requests.get(
|
|
515
|
+
request_url, headers=request_header, timeout=REQUEST_TIMEOUT
|
|
516
|
+
)
|
|
517
|
+
if response.ok:
|
|
518
|
+
return self.parse_request_response(response)
|
|
519
|
+
elif response.status_code == 401 and retries == 0:
|
|
520
|
+
logger.debug("Session has expired - try to re-authenticate...")
|
|
521
|
+
self.authenticate(revalidate=True)
|
|
522
|
+
request_header = self.request_header()
|
|
523
|
+
retries += 1
|
|
524
|
+
else:
|
|
525
|
+
logger.error(
|
|
526
|
+
"Failed to retrieve Salesforce object -> %s with %s = %s; status -> %s; error -> %s",
|
|
527
|
+
object_type,
|
|
528
|
+
search_field,
|
|
529
|
+
search_value,
|
|
530
|
+
response.status_code,
|
|
531
|
+
response.text,
|
|
532
|
+
)
|
|
533
|
+
return None
|
|
534
|
+
|
|
535
|
+
# end method definition
|
|
536
|
+
|
|
537
|
+
def add_object(self, object_type: str, **kwargs: Any) -> dict | None:
|
|
538
|
+
"""Add object to Salesforce. This is a generic wrapper method
|
|
539
|
+
for the actual add methods.
|
|
540
|
+
|
|
541
|
+
Args:
|
|
542
|
+
object_type (str): Type of the Salesforce business object, like "Account" or "Case".
|
|
543
|
+
**kwargs (dict): keyword / value ictionary with additional parameters
|
|
544
|
+
|
|
545
|
+
Returns:
|
|
546
|
+
dict | None: Dictionary with the Salesforce object data or None if the request fails.
|
|
547
|
+
"""
|
|
548
|
+
|
|
549
|
+
match object_type:
|
|
550
|
+
case "Account":
|
|
551
|
+
return self.add_account(
|
|
552
|
+
account_name=kwargs.pop("AccountName", None),
|
|
553
|
+
account_number=kwargs.pop("AccountNumber", None),
|
|
554
|
+
account_type=kwargs.pop("Type", None),
|
|
555
|
+
description=kwargs.pop("Description", None),
|
|
556
|
+
industry=kwargs.pop("Industry", None),
|
|
557
|
+
website=kwargs.pop("Website", None),
|
|
558
|
+
phone=kwargs.pop("Phone", None),
|
|
559
|
+
**kwargs,
|
|
560
|
+
)
|
|
561
|
+
case "Product":
|
|
562
|
+
return self.add_product(
|
|
563
|
+
product_name=kwargs.pop("Name", None),
|
|
564
|
+
product_code=kwargs.pop("ProductCode", None),
|
|
565
|
+
description=kwargs.pop("Description", None),
|
|
566
|
+
price=kwargs.pop("Price", None),
|
|
567
|
+
**kwargs,
|
|
568
|
+
)
|
|
569
|
+
case "Opportunity":
|
|
570
|
+
return self.add_opportunity(
|
|
571
|
+
name=kwargs.pop("Name", None),
|
|
572
|
+
stage=kwargs.pop("StageName", None),
|
|
573
|
+
close_date=kwargs.pop("CloseDate", None),
|
|
574
|
+
amount=kwargs.pop("Amount", None),
|
|
575
|
+
account_id=kwargs.pop("AccountId", None),
|
|
576
|
+
description=kwargs.pop("Description", None),
|
|
577
|
+
**kwargs,
|
|
578
|
+
)
|
|
579
|
+
case "Case":
|
|
580
|
+
return self.add_case(
|
|
581
|
+
subject=kwargs.pop("Subject", None),
|
|
582
|
+
description=kwargs.pop("Description", None),
|
|
583
|
+
status=kwargs.pop("Status", None),
|
|
584
|
+
priority=kwargs.pop("Priority", None),
|
|
585
|
+
origin=kwargs.pop("Origin", None),
|
|
586
|
+
account_id=kwargs.pop("AccountId", None),
|
|
587
|
+
owner_id=kwargs.pop("OwnerId", None),
|
|
588
|
+
asset_id=kwargs.pop("AssetId", None),
|
|
589
|
+
product_id=kwargs.pop("ProductId", None),
|
|
590
|
+
**kwargs,
|
|
591
|
+
)
|
|
592
|
+
case "Contract":
|
|
593
|
+
return self.add_contract(
|
|
594
|
+
account_id=kwargs.pop("AccountId", None),
|
|
595
|
+
start_date=kwargs.pop("ContractStartDate", None),
|
|
596
|
+
contract_term=kwargs.pop("ContractTerm", None),
|
|
597
|
+
status=kwargs.pop("Status", None),
|
|
598
|
+
description=kwargs.pop("Description", None),
|
|
599
|
+
contract_type=kwargs.pop("ContractType", None),
|
|
600
|
+
**kwargs,
|
|
601
|
+
)
|
|
602
|
+
case "Asset":
|
|
603
|
+
return self.add_asset(
|
|
604
|
+
asset_name=kwargs.pop("Name", None),
|
|
605
|
+
product_id=kwargs.pop("Product", None),
|
|
606
|
+
serial_number=kwargs.pop("SerialNumber", None),
|
|
607
|
+
status=kwargs.pop("Status", None),
|
|
608
|
+
purchase_date=kwargs.pop("PurchaseDate", None),
|
|
609
|
+
install_date=kwargs.pop("InstallDate", None),
|
|
610
|
+
description=kwargs.pop("AssetDescription", None),
|
|
611
|
+
**kwargs,
|
|
612
|
+
)
|
|
613
|
+
case _:
|
|
614
|
+
logger.error(
|
|
615
|
+
"Unsupported Salesforce business object -> %s!",
|
|
616
|
+
object_type,
|
|
617
|
+
)
|
|
618
|
+
|
|
619
|
+
# end method definition
|
|
620
|
+
|
|
621
|
+
def get_group_id(self, groupname: str) -> Optional[str]:
|
|
622
|
+
"""Get a group ID by group name.
|
|
623
|
+
|
|
624
|
+
Args:
|
|
625
|
+
groupname (str): Name of the Group.
|
|
626
|
+
|
|
627
|
+
Returns:
|
|
628
|
+
Optional[str]: Technical ID of the group
|
|
629
|
+
"""
|
|
630
|
+
|
|
631
|
+
return self.get_object_id_by_name(
|
|
632
|
+
object_type="Group", name=groupname, name_field="Name"
|
|
633
|
+
)
|
|
634
|
+
|
|
635
|
+
# end method definition
|
|
636
|
+
|
|
637
|
+
def get_group(self, group_id: str) -> dict | None:
|
|
638
|
+
"""Get a Salesforce group based on its ID.
|
|
639
|
+
|
|
640
|
+
Args:
|
|
641
|
+
group_id (str): ID of the Salesforce group
|
|
642
|
+
|
|
643
|
+
Returns:
|
|
644
|
+
dict | None: Dictionary with the Salesforce group data or None if the request fails.
|
|
645
|
+
"""
|
|
646
|
+
|
|
647
|
+
if not self._access_token or not self._instance_url:
|
|
648
|
+
self.authenticate()
|
|
649
|
+
|
|
650
|
+
request_header = self.request_header()
|
|
651
|
+
request_url = self.config()["groupUrl"] + group_id
|
|
652
|
+
|
|
653
|
+
logger.debug(
|
|
654
|
+
"Get Salesforce group with ID -> %s; calling -> %s", group_id, request_url
|
|
655
|
+
)
|
|
656
|
+
|
|
657
|
+
retries = 0
|
|
658
|
+
while True:
|
|
659
|
+
response = requests.get(
|
|
660
|
+
request_url, headers=request_header, timeout=REQUEST_TIMEOUT
|
|
661
|
+
)
|
|
662
|
+
if response.ok:
|
|
663
|
+
return self.parse_request_response(response)
|
|
664
|
+
elif response.status_code == 401 and retries == 0:
|
|
665
|
+
logger.debug("Session has expired - try to re-authenticate...")
|
|
666
|
+
self.authenticate(revalidate=True)
|
|
667
|
+
request_header = self.request_header()
|
|
668
|
+
retries += 1
|
|
669
|
+
else:
|
|
670
|
+
logger.error(
|
|
671
|
+
"Failed to get Salesforce group -> %s; status -> %s; error -> %s",
|
|
672
|
+
group_id,
|
|
673
|
+
response.status_code,
|
|
674
|
+
response.text,
|
|
675
|
+
)
|
|
676
|
+
return None
|
|
677
|
+
|
|
678
|
+
# end method definition
|
|
679
|
+
|
|
680
|
+
def add_group(
|
|
681
|
+
self,
|
|
682
|
+
group_name: str,
|
|
683
|
+
group_type: str = "Regular",
|
|
684
|
+
) -> dict | None:
|
|
685
|
+
"""Add a new Salesforce group.
|
|
686
|
+
|
|
687
|
+
Args:
|
|
688
|
+
group_name (str): Name of the new Salesforce group
|
|
689
|
+
|
|
690
|
+
Returns:
|
|
691
|
+
dict | None: Dictionary with the Salesforce Group data or None if the request fails.
|
|
692
|
+
|
|
693
|
+
Example response:
|
|
694
|
+
{
|
|
695
|
+
'id': '00GDn000000KWE0MAO',
|
|
696
|
+
'success': True,
|
|
697
|
+
'errors': []
|
|
698
|
+
}
|
|
699
|
+
"""
|
|
700
|
+
|
|
701
|
+
if not self._access_token or not self._instance_url:
|
|
702
|
+
self.authenticate()
|
|
703
|
+
|
|
704
|
+
request_header = self.request_header()
|
|
705
|
+
request_url = self.config()["groupUrl"]
|
|
706
|
+
|
|
707
|
+
payload = {"Name": group_name, "Type": group_type}
|
|
708
|
+
|
|
709
|
+
logger.debug(
|
|
710
|
+
"Adding Salesforce group -> %s; calling -> %s", group_name, request_url
|
|
711
|
+
)
|
|
712
|
+
|
|
713
|
+
retries = 0
|
|
714
|
+
while True:
|
|
715
|
+
response = requests.post(
|
|
716
|
+
request_url,
|
|
717
|
+
headers=request_header,
|
|
718
|
+
data=json.dumps(payload),
|
|
719
|
+
timeout=REQUEST_TIMEOUT,
|
|
720
|
+
)
|
|
721
|
+
if response.ok:
|
|
722
|
+
return self.parse_request_response(response)
|
|
723
|
+
elif response.status_code == 401 and retries == 0:
|
|
724
|
+
logger.debug("Session has expired - try to re-authenticate...")
|
|
725
|
+
self.authenticate(revalidate=True)
|
|
726
|
+
request_header = self.request_header()
|
|
727
|
+
retries += 1
|
|
728
|
+
else:
|
|
729
|
+
logger.error(
|
|
730
|
+
"Failed to add Salesforce group -> %s; status -> %s; error -> %s",
|
|
731
|
+
group_name,
|
|
732
|
+
response.status_code,
|
|
733
|
+
response.text,
|
|
734
|
+
)
|
|
735
|
+
return None
|
|
736
|
+
|
|
737
|
+
# end method definition
|
|
738
|
+
|
|
739
|
+
def update_group(
|
|
740
|
+
self,
|
|
741
|
+
group_id: str,
|
|
742
|
+
update_data: dict,
|
|
743
|
+
) -> dict:
|
|
744
|
+
"""Update a Salesforce group.
|
|
745
|
+
|
|
746
|
+
Args:
|
|
747
|
+
group_id (str): The Salesforce group ID.
|
|
748
|
+
update_data (dict): Dictionary containing the fields to update.
|
|
749
|
+
|
|
750
|
+
Returns:
|
|
751
|
+
dict: Response from the Salesforce API.
|
|
752
|
+
"""
|
|
753
|
+
|
|
754
|
+
if not self._access_token or not self._instance_url:
|
|
755
|
+
self.authenticate()
|
|
756
|
+
|
|
757
|
+
request_header = self.request_header()
|
|
758
|
+
|
|
759
|
+
request_url = self.config()["groupUrl"] + group_id
|
|
760
|
+
|
|
761
|
+
logger.debug(
|
|
762
|
+
"Update Salesforce group with ID -> %s; calling -> %s",
|
|
763
|
+
group_id,
|
|
764
|
+
request_url,
|
|
765
|
+
)
|
|
766
|
+
|
|
767
|
+
retries = 0
|
|
768
|
+
while True:
|
|
769
|
+
response = requests.patch(
|
|
770
|
+
request_url,
|
|
771
|
+
json=update_data,
|
|
772
|
+
headers=request_header,
|
|
773
|
+
timeout=REQUEST_TIMEOUT,
|
|
774
|
+
)
|
|
775
|
+
if response.ok:
|
|
776
|
+
return self.parse_request_response(response)
|
|
777
|
+
elif response.status_code == 401 and retries == 0:
|
|
778
|
+
logger.debug("Session has expired - try to re-authenticate...")
|
|
779
|
+
self.authenticate(revalidate=True)
|
|
780
|
+
request_header = self.request_header()
|
|
781
|
+
retries += 1
|
|
782
|
+
else:
|
|
783
|
+
logger.error(
|
|
784
|
+
"Failed to update Salesforce group -> %s; status -> %s; error -> %s",
|
|
785
|
+
group_id,
|
|
786
|
+
response.status_code,
|
|
787
|
+
response.text,
|
|
788
|
+
)
|
|
789
|
+
return None
|
|
790
|
+
|
|
791
|
+
# end method definition
|
|
792
|
+
|
|
793
|
+
def get_group_members(self, group_id: str) -> list | None:
|
|
794
|
+
"""Get Salesforce group members
|
|
795
|
+
|
|
796
|
+
Args:
|
|
797
|
+
group_id (str): Id of the group to retrieve the members
|
|
798
|
+
|
|
799
|
+
Returns:
|
|
800
|
+
list | None: result
|
|
801
|
+
|
|
802
|
+
Example response:
|
|
803
|
+
{
|
|
804
|
+
'totalSize': 1,
|
|
805
|
+
'done': True,
|
|
806
|
+
'records': [
|
|
807
|
+
{
|
|
808
|
+
'attributes': {
|
|
809
|
+
'type': 'GroupMember',
|
|
810
|
+
'url': '/services/data/v60.0/sobjects/GroupMember/011Dn000000ELhwIAG'
|
|
811
|
+
},
|
|
812
|
+
'UserOrGroupId': '00GDn000000KWE5MAO'
|
|
813
|
+
}
|
|
814
|
+
]
|
|
815
|
+
}
|
|
816
|
+
"""
|
|
817
|
+
|
|
818
|
+
if not self._access_token or not self._instance_url:
|
|
819
|
+
self.authenticate()
|
|
820
|
+
|
|
821
|
+
request_header = self.request_header()
|
|
822
|
+
|
|
823
|
+
request_url = self.config()["queryUrl"]
|
|
824
|
+
|
|
825
|
+
query = f"SELECT UserOrGroupId FROM GroupMember WHERE GroupId = '{group_id}'"
|
|
826
|
+
params = {"q": query}
|
|
827
|
+
|
|
828
|
+
logger.debug(
|
|
829
|
+
"Get members of Salesforce group with ID -> %s; calling -> %s",
|
|
830
|
+
group_id,
|
|
831
|
+
request_url,
|
|
832
|
+
)
|
|
833
|
+
|
|
834
|
+
retries = 0
|
|
835
|
+
while True:
|
|
836
|
+
response = requests.get(
|
|
837
|
+
request_url,
|
|
838
|
+
headers=request_header,
|
|
839
|
+
params=params,
|
|
840
|
+
timeout=REQUEST_TIMEOUT,
|
|
841
|
+
)
|
|
842
|
+
if response.ok:
|
|
843
|
+
return self.parse_request_response(response)
|
|
844
|
+
elif response.status_code == 401 and retries == 0:
|
|
845
|
+
logger.debug("Session has expired - try to re-authenticate...")
|
|
846
|
+
self.authenticate(revalidate=True)
|
|
847
|
+
request_header = self.request_header()
|
|
848
|
+
retries += 1
|
|
849
|
+
else:
|
|
850
|
+
logger.error(
|
|
851
|
+
"Failed to retrieve members of Salesforce group with ID -> %s; status -> %s; error -> %s",
|
|
852
|
+
group_id,
|
|
853
|
+
response.status_code,
|
|
854
|
+
response.text,
|
|
855
|
+
)
|
|
856
|
+
return None
|
|
857
|
+
|
|
858
|
+
# end method definition
|
|
859
|
+
|
|
860
|
+
def add_group_member(self, group_id: str, member_id: str) -> dict | None:
|
|
861
|
+
"""Add a user or group to a Salesforce group
|
|
862
|
+
|
|
863
|
+
Args:
|
|
864
|
+
group_id (str): ID of the Salesforce Group to add member to.
|
|
865
|
+
member_id (str): ID of the user or group.
|
|
866
|
+
|
|
867
|
+
Returns:
|
|
868
|
+
dict | None: Dictionary with the Salesforce membership data or None if the request fails.
|
|
869
|
+
|
|
870
|
+
Example response (id is the membership ID):
|
|
871
|
+
{
|
|
872
|
+
'id': '011Dn000000ELhwIAG',
|
|
873
|
+
'success': True,
|
|
874
|
+
'errors': []
|
|
875
|
+
}
|
|
876
|
+
"""
|
|
877
|
+
|
|
878
|
+
if not self._access_token or not self._instance_url:
|
|
879
|
+
self.authenticate()
|
|
880
|
+
|
|
881
|
+
request_url = self.config()["groupMemberUrl"]
|
|
882
|
+
|
|
883
|
+
request_header = self.request_header()
|
|
884
|
+
|
|
885
|
+
payload = {"GroupId": group_id, "UserOrGroupId": member_id}
|
|
886
|
+
|
|
887
|
+
logger.debug(
|
|
888
|
+
"Add member with ID -> %s to Salesforce group with ID -> %s; calling -> %s",
|
|
889
|
+
member_id,
|
|
890
|
+
group_id,
|
|
891
|
+
request_url,
|
|
892
|
+
)
|
|
893
|
+
|
|
894
|
+
retries = 0
|
|
895
|
+
while True:
|
|
896
|
+
response = requests.post(
|
|
897
|
+
request_url,
|
|
898
|
+
headers=request_header,
|
|
899
|
+
json=payload,
|
|
900
|
+
timeout=REQUEST_TIMEOUT,
|
|
901
|
+
)
|
|
902
|
+
if response.ok:
|
|
903
|
+
return self.parse_request_response(response)
|
|
904
|
+
elif response.status_code == 401 and retries == 0:
|
|
905
|
+
logger.debug("Session has expired - try to re-authenticate...")
|
|
906
|
+
self.authenticate(revalidate=True)
|
|
907
|
+
request_header = self.request_header()
|
|
908
|
+
retries += 1
|
|
909
|
+
else:
|
|
910
|
+
logger.error(
|
|
911
|
+
"Failed to retrieve members of Salesforce group with ID -> %s; status -> %s; error -> %s",
|
|
912
|
+
group_id,
|
|
913
|
+
response.status_code,
|
|
914
|
+
response.text,
|
|
915
|
+
)
|
|
916
|
+
return None
|
|
917
|
+
|
|
918
|
+
# end method definition
|
|
919
|
+
|
|
920
|
+
def get_all_user_profiles(self) -> dict | None:
|
|
921
|
+
"""Get all user profiles
|
|
922
|
+
|
|
923
|
+
Returns:
|
|
924
|
+
dict | None: Dictionary with salesforce user profiles.
|
|
925
|
+
|
|
926
|
+
Example response:
|
|
927
|
+
{
|
|
928
|
+
'totalSize': 15,
|
|
929
|
+
'done': True,
|
|
930
|
+
'records': [
|
|
931
|
+
{
|
|
932
|
+
...
|
|
933
|
+
'attributes': {
|
|
934
|
+
'type': 'Profile',
|
|
935
|
+
'url': '/services/data/v52.0/sobjects/Profile/00eDn000001msL8IAI'},
|
|
936
|
+
'Id': '00eDn000001msL8IAI',
|
|
937
|
+
'Name': 'Standard User',
|
|
938
|
+
'CreatedById':
|
|
939
|
+
'005Dn000001rRodIAE',
|
|
940
|
+
'CreatedDate': '2022-11-30T15:30:54.000+0000',
|
|
941
|
+
'Description': None,
|
|
942
|
+
'LastModifiedById': '005Dn000001rUacIAE',
|
|
943
|
+
'LastModifiedDate': '2024-02-08T17:46:17.000+0000',
|
|
944
|
+
'PermissionsCustomizeApplication': False,
|
|
945
|
+
'PermissionsEditTask': True,
|
|
946
|
+
'PermissionsImportLeads': False
|
|
947
|
+
}
|
|
948
|
+
}, ...
|
|
949
|
+
]
|
|
950
|
+
}
|
|
951
|
+
"""
|
|
952
|
+
|
|
953
|
+
if not self._access_token or not self._instance_url:
|
|
954
|
+
self.authenticate()
|
|
955
|
+
|
|
956
|
+
request_header = self.request_header()
|
|
957
|
+
request_url = self.config()["queryUrl"]
|
|
958
|
+
|
|
959
|
+
query = "SELECT Id, Name, CreatedById, CreatedDate, Description, LastModifiedById, LastModifiedDate, PermissionsCustomizeApplication, PermissionsEditTask, PermissionsImportLeads FROM Profile"
|
|
960
|
+
|
|
961
|
+
retries = 0
|
|
962
|
+
while True:
|
|
963
|
+
response = requests.get(
|
|
964
|
+
request_url,
|
|
965
|
+
headers=request_header,
|
|
966
|
+
params={"q": query},
|
|
967
|
+
timeout=REQUEST_TIMEOUT,
|
|
968
|
+
)
|
|
969
|
+
if response.ok:
|
|
970
|
+
return self.parse_request_response(response)
|
|
971
|
+
elif response.status_code == 401 and retries == 0:
|
|
972
|
+
logger.debug("Session has expired - try to re-authenticate...")
|
|
973
|
+
self.authenticate(revalidate=True)
|
|
974
|
+
request_header = self.request_header()
|
|
975
|
+
retries += 1
|
|
976
|
+
else:
|
|
977
|
+
logger.error(
|
|
978
|
+
"Failed to get Salesforce user profiles; status -> %s; error -> %s",
|
|
979
|
+
response.status_code,
|
|
980
|
+
response.text,
|
|
981
|
+
)
|
|
982
|
+
return None
|
|
983
|
+
|
|
984
|
+
# end method definition
|
|
985
|
+
|
|
986
|
+
def get_user_profile_id(self, profile_name: str) -> Optional[str]:
|
|
987
|
+
"""Get a user profile ID by profile name.
|
|
988
|
+
|
|
989
|
+
Args:
|
|
990
|
+
profile_name (str): Name of the User Profile.
|
|
991
|
+
|
|
992
|
+
Returns:
|
|
993
|
+
Optional[str]: Technical ID of the user profile.
|
|
994
|
+
"""
|
|
995
|
+
|
|
996
|
+
return self.get_object_id_by_name(object_type="Profile", name=profile_name)
|
|
997
|
+
|
|
998
|
+
# end method definition
|
|
999
|
+
|
|
1000
|
+
def get_user_id(self, username: str) -> Optional[str]:
|
|
1001
|
+
"""Get a user ID by user name.
|
|
1002
|
+
|
|
1003
|
+
Args:
|
|
1004
|
+
username (str): Name of the User.
|
|
1005
|
+
|
|
1006
|
+
Returns:
|
|
1007
|
+
Optional[str]: Technical ID of the user
|
|
1008
|
+
"""
|
|
1009
|
+
|
|
1010
|
+
return self.get_object_id_by_name(
|
|
1011
|
+
object_type="User", name=username, name_field="Username"
|
|
1012
|
+
)
|
|
1013
|
+
|
|
1014
|
+
# end method definition
|
|
1015
|
+
|
|
1016
|
+
def get_user(self, user_id: str) -> dict | None:
|
|
1017
|
+
"""Get a Salesforce user based on its ID.
|
|
1018
|
+
|
|
1019
|
+
Args:
|
|
1020
|
+
user_id (str): ID of the Salesforce user
|
|
1021
|
+
|
|
1022
|
+
Returns:
|
|
1023
|
+
dict | None: Dictionary with the Salesforce user data or None if the request fails.
|
|
1024
|
+
"""
|
|
1025
|
+
|
|
1026
|
+
if not self._access_token or not self._instance_url:
|
|
1027
|
+
self.authenticate()
|
|
1028
|
+
|
|
1029
|
+
request_header = self.request_header()
|
|
1030
|
+
request_url = self.config()["userUrl"] + user_id
|
|
1031
|
+
|
|
1032
|
+
logger.debug(
|
|
1033
|
+
"Get Salesforce user with ID -> %s; calling -> %s", user_id, request_url
|
|
1034
|
+
)
|
|
1035
|
+
|
|
1036
|
+
retries = 0
|
|
1037
|
+
while True:
|
|
1038
|
+
response = requests.get(
|
|
1039
|
+
request_url, headers=request_header, timeout=REQUEST_TIMEOUT
|
|
1040
|
+
)
|
|
1041
|
+
if response.ok:
|
|
1042
|
+
return self.parse_request_response(response)
|
|
1043
|
+
elif response.status_code == 401 and retries == 0:
|
|
1044
|
+
logger.debug("Session has expired - try to re-authenticate...")
|
|
1045
|
+
self.authenticate(revalidate=True)
|
|
1046
|
+
request_header = self.request_header()
|
|
1047
|
+
retries += 1
|
|
1048
|
+
else:
|
|
1049
|
+
logger.error(
|
|
1050
|
+
"Failed to get Salesforce user -> %s; status -> %s; error -> %s",
|
|
1051
|
+
user_id,
|
|
1052
|
+
response.status_code,
|
|
1053
|
+
response.text,
|
|
1054
|
+
)
|
|
1055
|
+
return None
|
|
1056
|
+
|
|
1057
|
+
# end method definition
|
|
1058
|
+
|
|
1059
|
+
def add_user(
|
|
1060
|
+
self,
|
|
1061
|
+
username: str,
|
|
1062
|
+
email: str,
|
|
1063
|
+
firstname: str,
|
|
1064
|
+
lastname: str,
|
|
1065
|
+
title: str | None = None,
|
|
1066
|
+
department: str | None = None,
|
|
1067
|
+
company_name: str = "Innovate",
|
|
1068
|
+
profile_name: Optional[str] = "Standard User",
|
|
1069
|
+
profile_id: Optional[str] = None,
|
|
1070
|
+
time_zone_key: Optional[str] = "America/Los_Angeles",
|
|
1071
|
+
email_encoding_key: Optional[str] = "ISO-8859-1",
|
|
1072
|
+
locale_key: Optional[str] = "en_US",
|
|
1073
|
+
alias: Optional[str] = None,
|
|
1074
|
+
) -> dict | None:
|
|
1075
|
+
"""Add a new Salesforce user. The password has to be set separately.
|
|
1076
|
+
|
|
1077
|
+
Args:
|
|
1078
|
+
username (str): Login name of the new user
|
|
1079
|
+
email (str): Email of the new user
|
|
1080
|
+
firstname (str): First name of the new user.
|
|
1081
|
+
lastname (str): Last name of the new user.
|
|
1082
|
+
title (str): Title of the user.
|
|
1083
|
+
department (str): Department of the user.
|
|
1084
|
+
company_name (str): Name of the Company of the user.
|
|
1085
|
+
profile_name (str): Profile name like "Standard User"
|
|
1086
|
+
profile_id (str, optional): Profile ID of the new user. Defaults to None.
|
|
1087
|
+
Use method get_all_user_profiles() to determine
|
|
1088
|
+
the desired Profile for the user. Or pass the profile_name.
|
|
1089
|
+
time_zone_key (str, optional) in format country/city like "America/Los_Angeles",
|
|
1090
|
+
email_encoding_key (str, optional). Default is "ISO-8859-1".
|
|
1091
|
+
locale_key (str, optional). Default is "en_US".
|
|
1092
|
+
alias (str, optional): Alias of the new user. Defaults to None.
|
|
1093
|
+
|
|
1094
|
+
Returns:
|
|
1095
|
+
dict | None: Dictionary with the Salesforce User data or None if the request fails.
|
|
1096
|
+
"""
|
|
1097
|
+
|
|
1098
|
+
if not self._access_token or not self._instance_url:
|
|
1099
|
+
self.authenticate()
|
|
1100
|
+
|
|
1101
|
+
request_header = self.request_header()
|
|
1102
|
+
request_url = self.config()["userUrl"]
|
|
1103
|
+
|
|
1104
|
+
# if just a profile name is given then we determine the profile ID by the name:
|
|
1105
|
+
if profile_name and not profile_id:
|
|
1106
|
+
profile_id = self.get_user_profile_id(profile_name)
|
|
1107
|
+
|
|
1108
|
+
payload = {
|
|
1109
|
+
"Username": username,
|
|
1110
|
+
"Email": email,
|
|
1111
|
+
"FirstName": firstname,
|
|
1112
|
+
"LastName": lastname,
|
|
1113
|
+
"ProfileId": profile_id,
|
|
1114
|
+
"Department": department,
|
|
1115
|
+
"CompanyName": company_name,
|
|
1116
|
+
"Title": title,
|
|
1117
|
+
"Alias": alias if alias else username,
|
|
1118
|
+
"TimeZoneSidKey": time_zone_key, # Set default TimeZoneSidKey
|
|
1119
|
+
"LocaleSidKey": locale_key, # Set default LocaleSidKey
|
|
1120
|
+
"EmailEncodingKey": email_encoding_key, # Set default EmailEncodingKey
|
|
1121
|
+
"LanguageLocaleKey": locale_key, # Set default LanguageLocaleKey
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
logger.debug(
|
|
1125
|
+
"Adding Salesforce user -> %s; calling -> %s", username, request_url
|
|
1126
|
+
)
|
|
1127
|
+
|
|
1128
|
+
retries = 0
|
|
1129
|
+
while True:
|
|
1130
|
+
response = requests.post(
|
|
1131
|
+
request_url,
|
|
1132
|
+
headers=request_header,
|
|
1133
|
+
data=json.dumps(payload),
|
|
1134
|
+
timeout=REQUEST_TIMEOUT,
|
|
1135
|
+
)
|
|
1136
|
+
if response.ok:
|
|
1137
|
+
return self.parse_request_response(response)
|
|
1138
|
+
elif response.status_code == 401 and retries == 0:
|
|
1139
|
+
logger.debug("Session has expired - try to re-authenticate...")
|
|
1140
|
+
self.authenticate(revalidate=True)
|
|
1141
|
+
request_header = self.request_header()
|
|
1142
|
+
retries += 1
|
|
1143
|
+
else:
|
|
1144
|
+
logger.error(
|
|
1145
|
+
"Failed to add Salesforce user -> %s; status -> %s; error -> %s",
|
|
1146
|
+
username,
|
|
1147
|
+
response.status_code,
|
|
1148
|
+
response.text,
|
|
1149
|
+
)
|
|
1150
|
+
return None
|
|
1151
|
+
|
|
1152
|
+
# end method definition
|
|
1153
|
+
|
|
1154
|
+
def update_user(
|
|
1155
|
+
self,
|
|
1156
|
+
user_id: str,
|
|
1157
|
+
update_data: dict,
|
|
1158
|
+
) -> dict:
|
|
1159
|
+
"""Update a Salesforce user.
|
|
1160
|
+
|
|
1161
|
+
Args:
|
|
1162
|
+
user_id (str): The Salesforce user ID.
|
|
1163
|
+
update_data (dict): Dictionary containing the fields to update.
|
|
1164
|
+
|
|
1165
|
+
Returns:
|
|
1166
|
+
dict: Response from the Salesforce API.
|
|
1167
|
+
"""
|
|
1168
|
+
|
|
1169
|
+
if not self._access_token or not self._instance_url:
|
|
1170
|
+
self.authenticate()
|
|
1171
|
+
|
|
1172
|
+
request_header = self.request_header()
|
|
1173
|
+
|
|
1174
|
+
request_url = self.config()["userUrl"] + user_id
|
|
1175
|
+
|
|
1176
|
+
logger.debug(
|
|
1177
|
+
"Update Salesforce user with ID -> %s; calling -> %s", user_id, request_url
|
|
1178
|
+
)
|
|
1179
|
+
|
|
1180
|
+
retries = 0
|
|
1181
|
+
while True:
|
|
1182
|
+
response = requests.patch(
|
|
1183
|
+
request_url,
|
|
1184
|
+
json=update_data,
|
|
1185
|
+
headers=request_header,
|
|
1186
|
+
timeout=REQUEST_TIMEOUT,
|
|
1187
|
+
)
|
|
1188
|
+
if response.ok:
|
|
1189
|
+
return self.parse_request_response(response)
|
|
1190
|
+
elif response.status_code == 401 and retries == 0:
|
|
1191
|
+
logger.debug("Session has expired - try to re-authenticate...")
|
|
1192
|
+
self.authenticate(revalidate=True)
|
|
1193
|
+
request_header = self.request_header()
|
|
1194
|
+
retries += 1
|
|
1195
|
+
else:
|
|
1196
|
+
logger.error(
|
|
1197
|
+
"Failed to update Salesforce user -> %s; status -> %s; error -> %s",
|
|
1198
|
+
user_id,
|
|
1199
|
+
response.status_code,
|
|
1200
|
+
response.text,
|
|
1201
|
+
)
|
|
1202
|
+
return None
|
|
1203
|
+
|
|
1204
|
+
# end method definition
|
|
1205
|
+
|
|
1206
|
+
def update_user_password(
|
|
1207
|
+
self,
|
|
1208
|
+
user_id: str,
|
|
1209
|
+
password: str,
|
|
1210
|
+
) -> dict:
|
|
1211
|
+
"""Update the password of a Salesforce user.
|
|
1212
|
+
|
|
1213
|
+
Args:
|
|
1214
|
+
user_id (str): The Salesforce user ID.
|
|
1215
|
+
password (str): New user password.
|
|
1216
|
+
|
|
1217
|
+
Returns:
|
|
1218
|
+
dict: Response from the Salesforce API.
|
|
1219
|
+
"""
|
|
1220
|
+
|
|
1221
|
+
if not self._access_token or not self._instance_url:
|
|
1222
|
+
self.authenticate()
|
|
1223
|
+
|
|
1224
|
+
request_header = self.request_header()
|
|
1225
|
+
|
|
1226
|
+
request_url = self.config()["userUrl"] + "{}/password".format(user_id)
|
|
1227
|
+
|
|
1228
|
+
logger.debug(
|
|
1229
|
+
"Update password of Salesforce user with ID -> %s; calling -> %s",
|
|
1230
|
+
user_id,
|
|
1231
|
+
request_url,
|
|
1232
|
+
)
|
|
1233
|
+
|
|
1234
|
+
update_data = {"NewPassword": password}
|
|
1235
|
+
|
|
1236
|
+
retries = 0
|
|
1237
|
+
while True:
|
|
1238
|
+
response = requests.post(
|
|
1239
|
+
request_url,
|
|
1240
|
+
json=update_data,
|
|
1241
|
+
headers=request_header,
|
|
1242
|
+
timeout=REQUEST_TIMEOUT,
|
|
1243
|
+
)
|
|
1244
|
+
if response.ok:
|
|
1245
|
+
return self.parse_request_response(response)
|
|
1246
|
+
elif response.status_code == 401 and retries == 0:
|
|
1247
|
+
logger.debug("Session has expired - try to re-authenticate...")
|
|
1248
|
+
self.authenticate(revalidate=True)
|
|
1249
|
+
request_header = self.request_header()
|
|
1250
|
+
retries += 1
|
|
1251
|
+
else:
|
|
1252
|
+
logger.error(
|
|
1253
|
+
"Failed to update password of Salesforce user -> %s; status -> %s; error -> %s",
|
|
1254
|
+
user_id,
|
|
1255
|
+
response.status_code,
|
|
1256
|
+
response.text,
|
|
1257
|
+
)
|
|
1258
|
+
return None
|
|
1259
|
+
|
|
1260
|
+
# end method definition
|
|
1261
|
+
|
|
1262
|
+
def update_user_photo(
|
|
1263
|
+
self,
|
|
1264
|
+
user_id: str,
|
|
1265
|
+
photo_path: str,
|
|
1266
|
+
) -> dict | None:
|
|
1267
|
+
"""Update the Salesforce user photo.
|
|
1268
|
+
|
|
1269
|
+
Args:
|
|
1270
|
+
user_id (str): Salesforce ID of the user
|
|
1271
|
+
photo_path (str): file system path with the location of the photo
|
|
1272
|
+
Returns:
|
|
1273
|
+
dict | None: Dictionary with the Salesforce User data or None if the request fails.
|
|
1274
|
+
"""
|
|
1275
|
+
|
|
1276
|
+
if not self._access_token or not self._instance_url:
|
|
1277
|
+
self.authenticate()
|
|
1278
|
+
|
|
1279
|
+
# Check if the photo file exists
|
|
1280
|
+
if not os.path.isfile(photo_path):
|
|
1281
|
+
logger.error("Photo file -> %s not found!", photo_path)
|
|
1282
|
+
return None
|
|
1283
|
+
|
|
1284
|
+
try:
|
|
1285
|
+
# Read the photo file as binary data
|
|
1286
|
+
with open(photo_path, "rb") as image_file:
|
|
1287
|
+
photo_data = image_file.read()
|
|
1288
|
+
except OSError as exception:
|
|
1289
|
+
# Handle any errors that occurred while reading the photo file
|
|
1290
|
+
logger.error(
|
|
1291
|
+
"Error reading photo file -> %s; error -> %s", photo_path, exception
|
|
1292
|
+
)
|
|
1293
|
+
return None
|
|
1294
|
+
|
|
1295
|
+
request_header = self.request_header(content_type=None)
|
|
1296
|
+
|
|
1297
|
+
data = {"json": json.dumps({"cropX": 0, "cropY": 0, "cropSize": 200})}
|
|
1298
|
+
request_url = self.config()["connectUrl"] + f"user-profiles/{user_id}/photo"
|
|
1299
|
+
files = {
|
|
1300
|
+
"fileUpload": (
|
|
1301
|
+
photo_path,
|
|
1302
|
+
photo_data,
|
|
1303
|
+
"application/octet-stream",
|
|
1304
|
+
)
|
|
1305
|
+
}
|
|
1306
|
+
|
|
1307
|
+
logger.debug(
|
|
1308
|
+
"Update profile photo of Salesforce user with ID -> %s; calling -> %s",
|
|
1309
|
+
user_id,
|
|
1310
|
+
request_url,
|
|
1311
|
+
)
|
|
1312
|
+
|
|
1313
|
+
retries = 0
|
|
1314
|
+
while True:
|
|
1315
|
+
response = requests.post(
|
|
1316
|
+
request_url,
|
|
1317
|
+
files=files,
|
|
1318
|
+
data=data,
|
|
1319
|
+
headers=request_header,
|
|
1320
|
+
verify=False,
|
|
1321
|
+
timeout=REQUEST_TIMEOUT,
|
|
1322
|
+
)
|
|
1323
|
+
if response.ok:
|
|
1324
|
+
return self.parse_request_response(response)
|
|
1325
|
+
elif response.status_code == 401 and retries == 0:
|
|
1326
|
+
logger.debug("Session has expired - try to re-authenticate...")
|
|
1327
|
+
self.authenticate(revalidate=True)
|
|
1328
|
+
request_header = self.request_header()
|
|
1329
|
+
retries += 1
|
|
1330
|
+
else:
|
|
1331
|
+
logger.error(
|
|
1332
|
+
"Failed to update profile photo of Salesforce user with ID -> %s; status -> %s; error -> %s",
|
|
1333
|
+
user_id,
|
|
1334
|
+
response.status_code,
|
|
1335
|
+
response.text,
|
|
1336
|
+
)
|
|
1337
|
+
return None
|
|
1338
|
+
|
|
1339
|
+
# end method definition
|
|
1340
|
+
|
|
1341
|
+
def add_account(
|
|
1342
|
+
self,
|
|
1343
|
+
account_name: str,
|
|
1344
|
+
account_number: str,
|
|
1345
|
+
account_type: str = "Customer",
|
|
1346
|
+
description: Optional[str] = None,
|
|
1347
|
+
industry: Optional[str] = None,
|
|
1348
|
+
website: Optional[str] = None,
|
|
1349
|
+
phone: Optional[str] = None,
|
|
1350
|
+
**kwargs: Any,
|
|
1351
|
+
) -> dict | None:
|
|
1352
|
+
"""Add a new Account object to Salesforce.
|
|
1353
|
+
|
|
1354
|
+
Args:
|
|
1355
|
+
account_name (str): Name of the new Salesforce account.
|
|
1356
|
+
account_number (str): Number of the new Salesforce account (this is a logical number, not the technical ID)
|
|
1357
|
+
account_type (str): Type of the Salesforce account. Typical values are "Customer" or "Prospect".
|
|
1358
|
+
description(str, optional): Description of the new Salesforce account.
|
|
1359
|
+
industry (str, optional): Industry of the new Salesforce account. Defaults to None.
|
|
1360
|
+
website (str, optional): Website of the new Salesforce account. Defaults to None.
|
|
1361
|
+
phone (str, optional): Phone number of the new Salesforce account. Defaults to None.
|
|
1362
|
+
kwargs (Any): Additional values (e.g. custom fields)
|
|
1363
|
+
|
|
1364
|
+
Returns:
|
|
1365
|
+
dict | None: Dictionary with the Salesforce Account data or None if the request fails.
|
|
1366
|
+
"""
|
|
1367
|
+
|
|
1368
|
+
if not self._access_token or not self._instance_url:
|
|
1369
|
+
self.authenticate()
|
|
1370
|
+
|
|
1371
|
+
request_header = self.request_header()
|
|
1372
|
+
request_url = self.config()["accountUrl"]
|
|
1373
|
+
|
|
1374
|
+
payload = {
|
|
1375
|
+
"Name": account_name,
|
|
1376
|
+
"AccountNumber": account_number,
|
|
1377
|
+
"Type": account_type,
|
|
1378
|
+
"Industry": industry,
|
|
1379
|
+
"Description": description,
|
|
1380
|
+
"Website": website,
|
|
1381
|
+
"Phone": phone,
|
|
1382
|
+
}
|
|
1383
|
+
payload.update(kwargs) # Add additional fields from kwargs
|
|
1384
|
+
|
|
1385
|
+
logger.debug(
|
|
1386
|
+
"Adding Salesforce account -> %s; calling -> %s", account_name, request_url
|
|
1387
|
+
)
|
|
1388
|
+
|
|
1389
|
+
retries = 0
|
|
1390
|
+
while True:
|
|
1391
|
+
response = requests.post(
|
|
1392
|
+
request_url,
|
|
1393
|
+
headers=request_header,
|
|
1394
|
+
data=json.dumps(payload),
|
|
1395
|
+
timeout=REQUEST_TIMEOUT,
|
|
1396
|
+
)
|
|
1397
|
+
if response.ok:
|
|
1398
|
+
return self.parse_request_response(response)
|
|
1399
|
+
elif response.status_code == 401 and retries == 0:
|
|
1400
|
+
logger.debug("Session has expired - try to re-authenticate...")
|
|
1401
|
+
self.authenticate(revalidate=True)
|
|
1402
|
+
request_header = self.request_header()
|
|
1403
|
+
retries += 1
|
|
1404
|
+
else:
|
|
1405
|
+
logger.error(
|
|
1406
|
+
"Failed to add Salesforce account -> %s; status -> %s; error -> %s",
|
|
1407
|
+
account_name,
|
|
1408
|
+
response.status_code,
|
|
1409
|
+
response.text,
|
|
1410
|
+
)
|
|
1411
|
+
return None
|
|
1412
|
+
|
|
1413
|
+
# end method definition
|
|
1414
|
+
|
|
1415
|
+
def add_product(
|
|
1416
|
+
self,
|
|
1417
|
+
product_name: str,
|
|
1418
|
+
product_code: str,
|
|
1419
|
+
description: str,
|
|
1420
|
+
price: float,
|
|
1421
|
+
**kwargs: Any,
|
|
1422
|
+
) -> dict | None:
|
|
1423
|
+
"""Add a new Product object to Salesforce.
|
|
1424
|
+
|
|
1425
|
+
Args:
|
|
1426
|
+
product_name (str): Name of the Salesforce Product.
|
|
1427
|
+
product_code (str): Code of the Salesforce Product.
|
|
1428
|
+
description (str): Description of the Salesforce Product.
|
|
1429
|
+
price (float): Price of the Salesforce Product.
|
|
1430
|
+
|
|
1431
|
+
Returns:
|
|
1432
|
+
dict | None: Dictionary with the Salesforce Product data or None if the request fails.
|
|
1433
|
+
"""
|
|
1434
|
+
|
|
1435
|
+
if not self._access_token or not self._instance_url:
|
|
1436
|
+
self.authenticate()
|
|
1437
|
+
|
|
1438
|
+
request_header = self.request_header()
|
|
1439
|
+
request_url = self.config()["productUrl"]
|
|
1440
|
+
|
|
1441
|
+
payload = {
|
|
1442
|
+
"Name": product_name,
|
|
1443
|
+
"ProductCode": product_code,
|
|
1444
|
+
"Description": description,
|
|
1445
|
+
"Price__c": price,
|
|
1446
|
+
}
|
|
1447
|
+
payload.update(kwargs) # Add additional fields from kwargs
|
|
1448
|
+
|
|
1449
|
+
logger.debug(
|
|
1450
|
+
"Add Salesforce product -> %s; calling -> %s", product_name, request_url
|
|
1451
|
+
)
|
|
1452
|
+
|
|
1453
|
+
retries = 0
|
|
1454
|
+
while True:
|
|
1455
|
+
response = requests.post(
|
|
1456
|
+
request_url,
|
|
1457
|
+
headers=request_header,
|
|
1458
|
+
data=json.dumps(payload),
|
|
1459
|
+
timeout=REQUEST_TIMEOUT,
|
|
1460
|
+
)
|
|
1461
|
+
if response.ok:
|
|
1462
|
+
return self.parse_request_response(response)
|
|
1463
|
+
elif response.status_code == 401 and retries == 0:
|
|
1464
|
+
logger.debug("Session has expired - try to re-authenticate...")
|
|
1465
|
+
self.authenticate(revalidate=True)
|
|
1466
|
+
request_header = self.request_header()
|
|
1467
|
+
retries += 1
|
|
1468
|
+
else:
|
|
1469
|
+
logger.error(
|
|
1470
|
+
"Failed to add Salesforce product -> %s; status -> %s; error -> %s",
|
|
1471
|
+
product_name,
|
|
1472
|
+
response.status_code,
|
|
1473
|
+
response.text,
|
|
1474
|
+
)
|
|
1475
|
+
return None
|
|
1476
|
+
|
|
1477
|
+
# end method definition
|
|
1478
|
+
|
|
1479
|
+
def add_opportunity(
|
|
1480
|
+
self,
|
|
1481
|
+
name: str,
|
|
1482
|
+
stage: str,
|
|
1483
|
+
close_date: str,
|
|
1484
|
+
amount: Union[int, float],
|
|
1485
|
+
account_id: str,
|
|
1486
|
+
description: str = None,
|
|
1487
|
+
**kwargs: Any,
|
|
1488
|
+
) -> dict | None:
|
|
1489
|
+
"""Add a new Opportunity object to Salesfoce.
|
|
1490
|
+
|
|
1491
|
+
Args:
|
|
1492
|
+
name (str): Name of the Opportunity.
|
|
1493
|
+
stage (str): Stage of the Opportunity. Typical Value:
|
|
1494
|
+
"Prospecting", "Qualification", "Value Proposition", "Negotiation/Review",
|
|
1495
|
+
"Closed Won", "Closed Lost"
|
|
1496
|
+
close_date (str): Close date of the Opportunity. Should be in format YYYY-MM-DD.
|
|
1497
|
+
amount (Union[int, float]): Amount (expected revenue) of the opportunity.
|
|
1498
|
+
Can either be an integer or a float value.
|
|
1499
|
+
account_id (str): Technical ID of the related Salesforce Account.
|
|
1500
|
+
|
|
1501
|
+
Returns:
|
|
1502
|
+
dict | None: Dictionary with the Salesforce Opportunity data or None if the request fails.
|
|
1503
|
+
"""
|
|
1504
|
+
|
|
1505
|
+
if not self._access_token or not self._instance_url:
|
|
1506
|
+
self.authenticate()
|
|
1507
|
+
|
|
1508
|
+
request_header = self.request_header()
|
|
1509
|
+
request_url = self.config()["opportunityUrl"]
|
|
1510
|
+
|
|
1511
|
+
payload = {
|
|
1512
|
+
"Name": name,
|
|
1513
|
+
"StageName": stage,
|
|
1514
|
+
"CloseDate": close_date,
|
|
1515
|
+
"Amount": amount,
|
|
1516
|
+
"AccountId": account_id,
|
|
1517
|
+
}
|
|
1518
|
+
if description:
|
|
1519
|
+
payload["Description"] = description
|
|
1520
|
+
payload.update(kwargs) # Add additional fields from kwargs
|
|
1521
|
+
|
|
1522
|
+
logger.debug(
|
|
1523
|
+
"Add Salesforce opportunity -> %s; calling -> %s", name, request_url
|
|
1524
|
+
)
|
|
1525
|
+
|
|
1526
|
+
retries = 0
|
|
1527
|
+
while True:
|
|
1528
|
+
response = requests.post(
|
|
1529
|
+
request_url,
|
|
1530
|
+
headers=request_header,
|
|
1531
|
+
data=json.dumps(payload),
|
|
1532
|
+
timeout=REQUEST_TIMEOUT,
|
|
1533
|
+
)
|
|
1534
|
+
if response.ok:
|
|
1535
|
+
return self.parse_request_response(response)
|
|
1536
|
+
elif response.status_code == 401 and retries == 0:
|
|
1537
|
+
logger.debug("Session has expired - try to re-authenticate...")
|
|
1538
|
+
self.authenticate(revalidate=True)
|
|
1539
|
+
request_header = self.request_header()
|
|
1540
|
+
retries += 1
|
|
1541
|
+
else:
|
|
1542
|
+
logger.error(
|
|
1543
|
+
"Failed to add Salesforce opportunity -> %s; status -> %s; error -> %s",
|
|
1544
|
+
name,
|
|
1545
|
+
response.status_code,
|
|
1546
|
+
response.text,
|
|
1547
|
+
)
|
|
1548
|
+
return None
|
|
1549
|
+
|
|
1550
|
+
# end method definition
|
|
1551
|
+
|
|
1552
|
+
def add_case(
|
|
1553
|
+
self,
|
|
1554
|
+
subject: str,
|
|
1555
|
+
description: str,
|
|
1556
|
+
status: str,
|
|
1557
|
+
priority: str,
|
|
1558
|
+
origin: str,
|
|
1559
|
+
account_id: str,
|
|
1560
|
+
owner_id: str,
|
|
1561
|
+
asset_id: Optional[str] = None,
|
|
1562
|
+
product_id: Optional[str] = None,
|
|
1563
|
+
**kwargs: Any,
|
|
1564
|
+
) -> dict | None:
|
|
1565
|
+
"""Add a new Case object to Salesforce. The case number is automatically created and can not be
|
|
1566
|
+
provided.
|
|
1567
|
+
|
|
1568
|
+
Args:
|
|
1569
|
+
subject (str): Subject (title) of the case. It's like the name.
|
|
1570
|
+
description (str): Description of the case
|
|
1571
|
+
status (str): Status of the case. Typecal values: "New", "On Hold", "Escalated"
|
|
1572
|
+
priority (str): Priority of the case. Typical values: "High", "Medium", "Low".
|
|
1573
|
+
origin (str): origin (source) of the case. Typical values: "Email", "Phone", "Web"
|
|
1574
|
+
account_id (str): technical ID of the related Account
|
|
1575
|
+
owner_id (str): owner of the case
|
|
1576
|
+
asset_id (str): technical ID of the related Asset
|
|
1577
|
+
product_id (str): technical ID of the related Product
|
|
1578
|
+
kwargs (Any): additional values (e.g. custom fields)
|
|
1579
|
+
|
|
1580
|
+
Returns:
|
|
1581
|
+
dict | None: Dictionary with the Salesforce Case data or None if the request fails.
|
|
1582
|
+
"""
|
|
1583
|
+
|
|
1584
|
+
if not self._access_token or not self._instance_url:
|
|
1585
|
+
self.authenticate()
|
|
1586
|
+
|
|
1587
|
+
request_header = self.request_header()
|
|
1588
|
+
request_url = self.config()["caseUrl"]
|
|
1589
|
+
|
|
1590
|
+
payload = {
|
|
1591
|
+
"Subject": subject,
|
|
1592
|
+
"Description": description,
|
|
1593
|
+
"Status": status,
|
|
1594
|
+
"Priority": priority,
|
|
1595
|
+
"Origin": origin,
|
|
1596
|
+
"AccountId": account_id,
|
|
1597
|
+
"OwnerId": owner_id,
|
|
1598
|
+
}
|
|
1599
|
+
|
|
1600
|
+
if asset_id:
|
|
1601
|
+
payload["AssetId"] = asset_id
|
|
1602
|
+
if product_id:
|
|
1603
|
+
payload["ProductId"] = product_id
|
|
1604
|
+
payload.update(kwargs) # Add additional fields from kwargs
|
|
1605
|
+
|
|
1606
|
+
logger.debug("Add Salesforce case -> %s; calling -> %s", subject, request_url)
|
|
1607
|
+
|
|
1608
|
+
retries = 0
|
|
1609
|
+
while True:
|
|
1610
|
+
response = requests.post(
|
|
1611
|
+
request_url,
|
|
1612
|
+
headers=request_header,
|
|
1613
|
+
data=json.dumps(payload),
|
|
1614
|
+
timeout=REQUEST_TIMEOUT,
|
|
1615
|
+
)
|
|
1616
|
+
if response.ok:
|
|
1617
|
+
return self.parse_request_response(response)
|
|
1618
|
+
elif response.status_code == 401 and retries == 0:
|
|
1619
|
+
logger.debug("Session has expired - try to re-authenticate...")
|
|
1620
|
+
self.authenticate(revalidate=True)
|
|
1621
|
+
request_header = self.request_header()
|
|
1622
|
+
retries += 1
|
|
1623
|
+
else:
|
|
1624
|
+
logger.error(
|
|
1625
|
+
"Failed to add Salesforce case -> %s; status -> %s; error -> %s",
|
|
1626
|
+
subject,
|
|
1627
|
+
response.status_code,
|
|
1628
|
+
response.text,
|
|
1629
|
+
)
|
|
1630
|
+
return None
|
|
1631
|
+
|
|
1632
|
+
# end method definition
|
|
1633
|
+
|
|
1634
|
+
def add_asset(
|
|
1635
|
+
self,
|
|
1636
|
+
asset_name: str,
|
|
1637
|
+
product_id: str,
|
|
1638
|
+
serial_number: str,
|
|
1639
|
+
status: str,
|
|
1640
|
+
purchase_date: str,
|
|
1641
|
+
install_date: str,
|
|
1642
|
+
description: str | None = None,
|
|
1643
|
+
**kwargs: Any,
|
|
1644
|
+
) -> dict | None:
|
|
1645
|
+
"""Add a new Asset object to Salesforce.
|
|
1646
|
+
|
|
1647
|
+
Args:
|
|
1648
|
+
asset_name (str): Name of the Asset.
|
|
1649
|
+
product_id (str): Related Product ID.
|
|
1650
|
+
serial_number (str): Serial Number of the Asset.
|
|
1651
|
+
status (str): Status of the Asset. Typical values are "Purchased", "Shipped", "Installed", "Registered", "Obsolete"
|
|
1652
|
+
purchase_date (str): Purchase date of the Asset.
|
|
1653
|
+
install_date (str): Install date of the Asset.
|
|
1654
|
+
description (str): Description of the Asset.
|
|
1655
|
+
kwargs (Any): Additional values (e.g. custom fields)
|
|
1656
|
+
|
|
1657
|
+
Returns:
|
|
1658
|
+
dict | None: Dictionary with the Salesforce Asset data or None if the request fails.
|
|
1659
|
+
"""
|
|
1660
|
+
|
|
1661
|
+
if not self._access_token or not self._instance_url:
|
|
1662
|
+
self.authenticate()
|
|
1663
|
+
|
|
1664
|
+
request_header = self.request_header()
|
|
1665
|
+
request_url = self.config()["assetUrl"]
|
|
1666
|
+
|
|
1667
|
+
payload = {
|
|
1668
|
+
"Name": asset_name,
|
|
1669
|
+
"ProductId": product_id,
|
|
1670
|
+
"SerialNumber": serial_number,
|
|
1671
|
+
"Status": status,
|
|
1672
|
+
"PurchaseDate": purchase_date,
|
|
1673
|
+
"InstallDate": install_date,
|
|
1674
|
+
}
|
|
1675
|
+
if description:
|
|
1676
|
+
payload["Description"] = description
|
|
1677
|
+
payload.update(kwargs) # Add additional fields from kwargs
|
|
1678
|
+
|
|
1679
|
+
logger.debug(
|
|
1680
|
+
"Add Salesforce asset -> %s; calling -> %s", asset_name, request_url
|
|
1681
|
+
)
|
|
1682
|
+
|
|
1683
|
+
retries = 0
|
|
1684
|
+
while True:
|
|
1685
|
+
response = requests.post(
|
|
1686
|
+
request_url,
|
|
1687
|
+
headers=request_header,
|
|
1688
|
+
data=json.dumps(payload),
|
|
1689
|
+
timeout=REQUEST_TIMEOUT,
|
|
1690
|
+
)
|
|
1691
|
+
if response.ok:
|
|
1692
|
+
return self.parse_request_response(response)
|
|
1693
|
+
elif response.status_code == 401 and retries == 0:
|
|
1694
|
+
logger.debug("Session has expired - try to re-authenticate...")
|
|
1695
|
+
self.authenticate(revalidate=True)
|
|
1696
|
+
request_header = self.request_header()
|
|
1697
|
+
retries += 1
|
|
1698
|
+
else:
|
|
1699
|
+
logger.error(
|
|
1700
|
+
"Failed to add Salesforce user -> %s; status -> %s; error -> %s",
|
|
1701
|
+
asset_name,
|
|
1702
|
+
response.status_code,
|
|
1703
|
+
response.text,
|
|
1704
|
+
)
|
|
1705
|
+
return None
|
|
1706
|
+
|
|
1707
|
+
# end method definition
|
|
1708
|
+
|
|
1709
|
+
def add_contract(
|
|
1710
|
+
self,
|
|
1711
|
+
account_id: str,
|
|
1712
|
+
start_date: str,
|
|
1713
|
+
contract_term: int,
|
|
1714
|
+
status: str = "Draft",
|
|
1715
|
+
description: Optional[str] = None,
|
|
1716
|
+
contract_type: Optional[str] = None,
|
|
1717
|
+
**kwargs: Any,
|
|
1718
|
+
) -> dict | None:
|
|
1719
|
+
"""Add a new Contract object to Salesforce.
|
|
1720
|
+
|
|
1721
|
+
Args:
|
|
1722
|
+
account_id (str): Technical ID of the related Salesforce Account object.
|
|
1723
|
+
start_date (str): Start date of the Contract. Use YYYY-MM-DD notation.
|
|
1724
|
+
contract_term (int): Term of the Contract in number of months, e.g. 48 for 4 years term.
|
|
1725
|
+
The end date of the contract will be calculated from start date + term.
|
|
1726
|
+
contract_type (str): Type of the Contract. Typical values are "Subscription",
|
|
1727
|
+
"Maintenance", "Support", "Lease", or "Service".
|
|
1728
|
+
status (str): Status of the Contract. Typical values are "Draft", "Activated", or "In Approval Process"
|
|
1729
|
+
|
|
1730
|
+
Returns:
|
|
1731
|
+
dict | None: Dictionary with the Salesforce user data or None if the request fails.
|
|
1732
|
+
"""
|
|
1733
|
+
|
|
1734
|
+
if not self._access_token or not self._instance_url:
|
|
1735
|
+
self.authenticate()
|
|
1736
|
+
|
|
1737
|
+
request_header = self.request_header()
|
|
1738
|
+
request_url = self.config()["contractUrl"]
|
|
1739
|
+
|
|
1740
|
+
payload = {
|
|
1741
|
+
"AccountId": account_id,
|
|
1742
|
+
"StartDate": start_date,
|
|
1743
|
+
"ContractTerm": contract_term,
|
|
1744
|
+
"Status": status,
|
|
1745
|
+
}
|
|
1746
|
+
if description:
|
|
1747
|
+
payload["Description"] = description
|
|
1748
|
+
if contract_type:
|
|
1749
|
+
payload["ContractType"] = contract_type
|
|
1750
|
+
payload.update(kwargs) # Add additional fields from kwargs
|
|
1751
|
+
|
|
1752
|
+
logger.debug(
|
|
1753
|
+
"Adding Salesforce contract for account ID -> %s; calling -> %s",
|
|
1754
|
+
account_id,
|
|
1755
|
+
request_url,
|
|
1756
|
+
)
|
|
1757
|
+
|
|
1758
|
+
retries = 0
|
|
1759
|
+
while True:
|
|
1760
|
+
response = requests.post(
|
|
1761
|
+
request_url,
|
|
1762
|
+
headers=request_header,
|
|
1763
|
+
data=json.dumps(payload),
|
|
1764
|
+
timeout=REQUEST_TIMEOUT,
|
|
1765
|
+
)
|
|
1766
|
+
if response.ok:
|
|
1767
|
+
return self.parse_request_response(response)
|
|
1768
|
+
elif response.status_code == 401 and retries == 0:
|
|
1769
|
+
logger.debug("Session has expired - try to re-authenticate...")
|
|
1770
|
+
self.authenticate(revalidate=True)
|
|
1771
|
+
request_header = self.request_header()
|
|
1772
|
+
retries += 1
|
|
1773
|
+
else:
|
|
1774
|
+
logger.error(
|
|
1775
|
+
"Failed to add Salesforce contract for account ID -> %s; status -> %s; error -> %s",
|
|
1776
|
+
account_id,
|
|
1777
|
+
response.status_code,
|
|
1778
|
+
response.text,
|
|
1779
|
+
)
|
|
1780
|
+
return None
|
|
1781
|
+
|
|
1782
|
+
# end method definition
|