pyxecm 1.5__py3-none-any.whl → 2.0.0__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.

Files changed (56) hide show
  1. pyxecm/__init__.py +6 -2
  2. pyxecm/avts.py +1492 -0
  3. pyxecm/coreshare.py +1075 -960
  4. pyxecm/customizer/__init__.py +16 -4
  5. pyxecm/customizer/__main__.py +58 -0
  6. pyxecm/customizer/api/__init__.py +5 -0
  7. pyxecm/customizer/api/__main__.py +6 -0
  8. pyxecm/customizer/api/app.py +914 -0
  9. pyxecm/customizer/api/auth.py +154 -0
  10. pyxecm/customizer/api/metrics.py +92 -0
  11. pyxecm/customizer/api/models.py +13 -0
  12. pyxecm/customizer/api/payload_list.py +865 -0
  13. pyxecm/customizer/api/settings.py +103 -0
  14. pyxecm/customizer/browser_automation.py +332 -139
  15. pyxecm/customizer/customizer.py +1075 -1057
  16. pyxecm/customizer/exceptions.py +35 -0
  17. pyxecm/customizer/guidewire.py +322 -0
  18. pyxecm/customizer/k8s.py +787 -338
  19. pyxecm/customizer/log.py +107 -0
  20. pyxecm/customizer/m365.py +3424 -2270
  21. pyxecm/customizer/nhc.py +1169 -0
  22. pyxecm/customizer/openapi.py +258 -0
  23. pyxecm/customizer/payload.py +18201 -7030
  24. pyxecm/customizer/pht.py +1047 -210
  25. pyxecm/customizer/salesforce.py +836 -727
  26. pyxecm/customizer/sap.py +58 -41
  27. pyxecm/customizer/servicenow.py +851 -383
  28. pyxecm/customizer/settings.py +442 -0
  29. pyxecm/customizer/successfactors.py +408 -346
  30. pyxecm/customizer/translate.py +83 -48
  31. pyxecm/helper/__init__.py +5 -2
  32. pyxecm/helper/assoc.py +98 -38
  33. pyxecm/helper/data.py +2482 -742
  34. pyxecm/helper/logadapter.py +27 -0
  35. pyxecm/helper/web.py +229 -101
  36. pyxecm/helper/xml.py +528 -172
  37. pyxecm/maintenance_page/__init__.py +5 -0
  38. pyxecm/maintenance_page/__main__.py +6 -0
  39. pyxecm/maintenance_page/app.py +51 -0
  40. pyxecm/maintenance_page/settings.py +28 -0
  41. pyxecm/maintenance_page/static/favicon.avif +0 -0
  42. pyxecm/maintenance_page/templates/maintenance.html +165 -0
  43. pyxecm/otac.py +234 -140
  44. pyxecm/otawp.py +2689 -0
  45. pyxecm/otcs.py +12344 -7547
  46. pyxecm/otds.py +3166 -2219
  47. pyxecm/otiv.py +36 -21
  48. pyxecm/otmm.py +1363 -296
  49. pyxecm/otpd.py +231 -127
  50. pyxecm-2.0.0.dist-info/METADATA +145 -0
  51. pyxecm-2.0.0.dist-info/RECORD +54 -0
  52. {pyxecm-1.5.dist-info → pyxecm-2.0.0.dist-info}/WHEEL +1 -1
  53. pyxecm-1.5.dist-info/METADATA +0 -51
  54. pyxecm-1.5.dist-info/RECORD +0 -30
  55. {pyxecm-1.5.dist-info → pyxecm-2.0.0.dist-info/licenses}/LICENSE +0 -0
  56. {pyxecm-1.5.dist-info → pyxecm-2.0.0.dist-info}/top_level.txt +0 -0
@@ -1,66 +1,23 @@
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
1
+ """Salesforce Module to interact with the Salesforce API.
4
2
 
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.
3
+ See: https://developer.salesforce.com/docs/atlas.en-us.api_rest.meta/api_rest/intro_rest.htm
48
4
  """
49
5
 
50
6
  __author__ = "Dr. Marc Diefenbruch"
51
- __copyright__ = "Copyright 2024, OpenText"
7
+ __copyright__ = "Copyright (C) 2024-2025, OpenText"
52
8
  __credits__ = ["Kai-Philip Gatzweiler"]
53
9
  __maintainer__ = "Dr. Marc Diefenbruch"
54
10
  __email__ = "mdiefenb@opentext.com"
55
11
 
56
- import os
57
12
  import json
58
13
  import logging
14
+ import os
15
+ import time
16
+ from http import HTTPStatus
59
17
 
60
- from typing import Optional, Union, Any
61
18
  import requests
62
19
 
63
- logger = logging.getLogger("pyxecm.customizer.salesforce")
20
+ default_logger = logging.getLogger("pyxecm.customizer.salesforce")
64
21
 
65
22
  REQUEST_LOGIN_HEADERS = {
66
23
  "Content-Type": "application/x-www-form-urlencoded",
@@ -68,10 +25,16 @@ REQUEST_LOGIN_HEADERS = {
68
25
  }
69
26
 
70
27
  REQUEST_TIMEOUT = 60
28
+ REQUEST_RETRY_DELAY = 20
29
+ REQUEST_MAX_RETRIES = 3
30
+
71
31
  SALESFORCE_API_VERSION = "v60.0"
72
32
 
73
- class Salesforce(object):
74
- """Used to retrieve and automate stettings in Salesforce."""
33
+
34
+ class Salesforce:
35
+ """Class Salesforce is used to retrieve and automate stettings and objects in Salesforce."""
36
+
37
+ logger: logging.Logger = default_logger
75
38
 
76
39
  _config: dict
77
40
  _access_token = None
@@ -86,21 +49,38 @@ class Salesforce(object):
86
49
  password: str,
87
50
  authorization_url: str = "",
88
51
  security_token: str = "",
89
- ):
90
- """Initialize the Salesforce object
52
+ logger: logging.Logger = default_logger,
53
+ ) -> None:
54
+ """Initialize the Salesforce object.
91
55
 
92
56
  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
57
+ base_url (str):
58
+ Base URL of the Salesforce tenant.
59
+ authorization_url (str):
60
+ Authorization URL of the Salesforce tenant, typically ending with "/services/oauth2/token".
61
+ client_id (str):
62
+ The Salesforce Client ID.
63
+ client_secret (str):
64
+ The Salesforce Client Secret.
65
+ username (str):
66
+ User name in Saleforce used by the REST API.
67
+ password (str):
68
+ Password of the user used by the REST API.
69
+ authorization_url (str, optional):
70
+ URL for Salesforce login. If not given it will be constructed with default values
71
+ using base_url.
72
+ security_token (str, optional):
73
+ Security token for Salesforce login.
74
+ logger (logging.Logger, optional):
75
+ The logging object to use for all log messages. Defaults to default_logger.
76
+
102
77
  """
103
78
 
79
+ if logger != default_logger:
80
+ self.logger = logger.getChild("salesforce")
81
+ for logfilter in logger.filters:
82
+ self.logger.addFilter(logfilter)
83
+
104
84
  # The instance URL is also returned by the authenticate call
105
85
  # but typically it is identical to the base_url.
106
86
  self._instance_url = base_url
@@ -116,38 +96,32 @@ class Salesforce(object):
116
96
 
117
97
  # Set the Salesforce URLs and REST API endpoints:
118
98
  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)
99
+ salesforce_config["objectUrl"] = salesforce_config["baseUrl"] + "/services/data/{}/sobjects/".format(
100
+ SALESFORCE_API_VERSION,
101
+ )
102
+ salesforce_config["queryUrl"] = salesforce_config["baseUrl"] + "/services/data/{}/query/".format(
103
+ SALESFORCE_API_VERSION,
104
+ )
105
+ salesforce_config["compositeUrl"] = salesforce_config["baseUrl"] + "/services/data/{}/composite/".format(
106
+ SALESFORCE_API_VERSION,
107
+ )
108
+ salesforce_config["connectUrl"] = salesforce_config["baseUrl"] + "/services/data/{}/connect/".format(
109
+ SALESFORCE_API_VERSION,
110
+ )
111
+ salesforce_config["toolingUrl"] = salesforce_config["baseUrl"] + "/services/data/{}/tooling/".format(
112
+ SALESFORCE_API_VERSION,
113
+ )
134
114
  if authorization_url:
135
115
  salesforce_config["authenticationUrl"] = authorization_url
136
116
  else:
137
- salesforce_config["authenticationUrl"] = (
138
- salesforce_config["baseUrl"] + "/services/oauth2/token"
139
- )
117
+ salesforce_config["authenticationUrl"] = salesforce_config["baseUrl"] + "/services/oauth2/token"
140
118
  # URLs that are based on the objectURL (sobjects/):
141
119
  salesforce_config["userUrl"] = salesforce_config["objectUrl"] + "User/"
142
120
  salesforce_config["groupUrl"] = salesforce_config["objectUrl"] + "Group/"
143
- salesforce_config["groupMemberUrl"] = (
144
- salesforce_config["objectUrl"] + "GroupMember/"
145
- )
121
+ salesforce_config["groupMemberUrl"] = salesforce_config["objectUrl"] + "GroupMember/"
146
122
  salesforce_config["accountUrl"] = salesforce_config["objectUrl"] + "Account/"
147
123
  salesforce_config["productUrl"] = salesforce_config["objectUrl"] + "Product2/"
148
- salesforce_config["opportunityUrl"] = (
149
- salesforce_config["objectUrl"] + "Opportunity/"
150
- )
124
+ salesforce_config["opportunityUrl"] = salesforce_config["objectUrl"] + "Opportunity/"
151
125
  salesforce_config["caseUrl"] = salesforce_config["objectUrl"] + "Case/"
152
126
  salesforce_config["assetUrl"] = salesforce_config["objectUrl"] + "Asset/"
153
127
  salesforce_config["contractUrl"] = salesforce_config["objectUrl"] + "Contract/"
@@ -166,33 +140,44 @@ class Salesforce(object):
166
140
  # end method definition
167
141
 
168
142
  def config(self) -> dict:
169
- """Returns the configuration dictionary
143
+ """Return the configuration dictionary.
170
144
 
171
145
  Returns:
172
- dict: Configuration dictionary
146
+ dict:
147
+ The configuration dictionary.
148
+
173
149
  """
150
+
174
151
  return self._config
175
152
 
176
153
  # end method definition
177
154
 
178
155
  def credentials(self) -> dict:
179
- """Return the login credentials
156
+ """Return the login credentials.
180
157
 
181
158
  Returns:
182
- dict: dictionary with login credentials for Salesforce
159
+ dict:
160
+ The dictionary with login credentials for Salesforce.
161
+
183
162
  """
163
+
184
164
  return self.config()["authenticationData"]
185
165
 
186
166
  # end method definition
187
167
 
188
168
  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
169
+ """Return the request header used for Application calls.
170
+
171
+ Consists of Bearer access token and Content Type
191
172
 
192
173
  Args:
193
- content_type (str, optional): content type for the request
194
- Return:
195
- dict: request header values
174
+ content_type (str, optional):
175
+ Content type for the request. Default is "pplication/json".
176
+
177
+ Returns:
178
+ dict:
179
+ The equest header values
180
+
196
181
  """
197
182
 
198
183
  request_header = {
@@ -205,49 +190,254 @@ class Salesforce(object):
205
190
 
206
191
  # end method definition
207
192
 
193
+ def do_request(
194
+ self,
195
+ url: str,
196
+ method: str = "GET",
197
+ headers: dict | None = None,
198
+ data: dict | None = None,
199
+ json_data: dict | None = None,
200
+ files: dict | None = None,
201
+ params: dict | None = None,
202
+ timeout: int | None = REQUEST_TIMEOUT,
203
+ show_error: bool = True,
204
+ show_warning: bool = False,
205
+ warning_message: str = "",
206
+ failure_message: str = "",
207
+ success_message: str = "",
208
+ max_retries: int = REQUEST_MAX_RETRIES,
209
+ retry_forever: bool = False,
210
+ parse_request_response: bool = True,
211
+ stream: bool = False,
212
+ verify: bool = True,
213
+ ) -> dict | None:
214
+ """Call an Salesforce REST API in a safe way.
215
+
216
+ Args:
217
+ url (str):
218
+ The URL to send the request to.
219
+ method (str, optional):
220
+ HTTP method (GET, POST, etc.). Defaults to "GET".
221
+ headers (dict | None, optional):
222
+ Request Headers. Defaults to None.
223
+ data (dict | None, optional):
224
+ Request payload. Defaults to None
225
+ json_data (dict | None, optional):
226
+ Request payload. Defaults to None.
227
+ files (dict | None, optional):
228
+ Dictionary of {"name": file-tuple} for multipart encoding upload.
229
+ The file-tuple can be a 2-tuple ("filename", fileobj) or a 3-tuple ("filename", fileobj, "content_type")
230
+ params (dict | None, optional):
231
+ Add key-value pairs to the query string of the URL.
232
+ When you use the params parameter, requests automatically appends
233
+ the key-value pairs to the URL as part of the query string
234
+ timeout (int | None, optional):
235
+ Timeout for the request in seconds. Defaults to REQUEST_TIMEOUT.
236
+ show_error (bool, optional):
237
+ Whether or not an error should be logged in case of a failed REST call.
238
+ If False, then only a warning is logged. Defaults to True.
239
+ show_warning (bool, optional):
240
+ Whether or not an warning should be logged in case of a failed REST call.
241
+ If False, then only a warning is logged. Defaults to True.
242
+ warning_message (str, optional):
243
+ Specific warning message. Defaults to "".
244
+ If not given the error_message will be used.
245
+ failure_message (str, optional):
246
+ Specific error message. Defaults to "".
247
+ success_message (str, optional):
248
+ Specific success message. Defaults to "".
249
+ max_retries (int, optional):
250
+ How many retries on Connection errors? Default is REQUEST_MAX_RETRIES.
251
+ retry_forever (bool, optional):
252
+ Eventually wait forever - without timeout. Defaults to False.
253
+ parse_request_response (bool, optional):
254
+ Should the response.text be interpreted as json and loaded into a dictionary.
255
+ True is the default.
256
+ stream (bool, optional):
257
+ Control whether the response content should be immediately downloaded or streamed incrementally.
258
+ verify (bool, optional):
259
+ Specify whether or not SSL certificates should be verified when making an HTTPS request.
260
+ Default = True
261
+
262
+ Returns:
263
+ dict | None:
264
+ Response of OTDS REST API or None in case of an error.
265
+
266
+ """
267
+
268
+ if headers is None:
269
+ self.logger.error(
270
+ "Missing request header. Cannot send request to Core Share!",
271
+ )
272
+ return None
273
+
274
+ # In case of an expired session we reauthenticate and
275
+ # try 1 more time. Session expiration should not happen
276
+ # twice in a row:
277
+ retries = 0
278
+
279
+ while True:
280
+ try:
281
+ response = requests.request(
282
+ method=method,
283
+ url=url,
284
+ data=data,
285
+ json=json_data,
286
+ files=files,
287
+ params=params,
288
+ headers=headers,
289
+ timeout=timeout,
290
+ stream=stream,
291
+ verify=verify,
292
+ )
293
+
294
+ if response.ok:
295
+ if success_message:
296
+ self.logger.info(success_message)
297
+ if parse_request_response:
298
+ return self.parse_request_response(response)
299
+ else:
300
+ return response
301
+ # Check if Session has expired - then re-authenticate and try once more
302
+ elif response.status_code == 401 and retries == 0:
303
+ self.logger.debug("Session has expired - try to re-authenticate...")
304
+ self.authenticate(revalidate=True)
305
+ # Make sure to not change an existing content type
306
+ # the do_request() method is called with:
307
+ headers = self.request_header(
308
+ content_type=headers.get("Content-Type", None),
309
+ )
310
+ retries += 1
311
+ else:
312
+ # Handle plain HTML responses to not pollute the logs
313
+ content_type = response.headers.get("content-type", None)
314
+ if content_type == "text/html":
315
+ response_text = "HTML content (only printed in debug log)"
316
+ else:
317
+ response_text = response.text
318
+
319
+ if show_error:
320
+ self.logger.error(
321
+ "%s; status -> %s/%s; error -> %s",
322
+ failure_message,
323
+ response.status_code,
324
+ HTTPStatus(response.status_code).phrase,
325
+ response_text,
326
+ )
327
+ elif show_warning:
328
+ self.logger.warning(
329
+ "%s; status -> %s/%s; warning -> %s",
330
+ warning_message if warning_message else failure_message,
331
+ response.status_code,
332
+ HTTPStatus(response.status_code).phrase,
333
+ response_text,
334
+ )
335
+ if content_type == "text/html":
336
+ self.logger.debug(
337
+ "%s; status -> %s/%s; warning -> %s",
338
+ failure_message,
339
+ response.status_code,
340
+ HTTPStatus(response.status_code).phrase,
341
+ response.text,
342
+ )
343
+ return None
344
+ except requests.exceptions.Timeout:
345
+ if retries <= max_retries:
346
+ self.logger.warning(
347
+ "Request timed out. Retrying in %s seconds...",
348
+ str(REQUEST_RETRY_DELAY),
349
+ )
350
+ retries += 1
351
+ time.sleep(REQUEST_RETRY_DELAY) # Add a delay before retrying
352
+ else:
353
+ self.logger.error(
354
+ "%s; timeout error.",
355
+ failure_message,
356
+ )
357
+ if retry_forever:
358
+ # If it fails after REQUEST_MAX_RETRIES retries we let it wait forever
359
+ self.logger.warning("Turn timeouts off and wait forever...")
360
+ timeout = None
361
+ else:
362
+ return None
363
+ except requests.exceptions.ConnectionError:
364
+ if retries <= max_retries:
365
+ self.logger.warning(
366
+ "Connection error. Retrying in %s seconds...",
367
+ str(REQUEST_RETRY_DELAY),
368
+ )
369
+ retries += 1
370
+ time.sleep(REQUEST_RETRY_DELAY) # Add a delay before retrying
371
+ else:
372
+ self.logger.error(
373
+ "%s; connection error.",
374
+ failure_message,
375
+ )
376
+ if retry_forever:
377
+ # If it fails after REQUEST_MAX_RETRIES retries we let it wait forever
378
+ self.logger.warning("Turn timeouts off and wait forever...")
379
+ timeout = None
380
+ time.sleep(REQUEST_RETRY_DELAY) # Add a delay before retrying
381
+ else:
382
+ return None
383
+ # end try
384
+ self.logger.debug(
385
+ "Retrying REST API %s call -> %s... (retry = %s)",
386
+ method,
387
+ url,
388
+ str(retries),
389
+ )
390
+ # end while True
391
+
392
+ # end method definition
393
+
208
394
  def parse_request_response(
209
395
  self,
210
396
  response_object: requests.Response,
211
397
  additional_error_message: str = "",
212
398
  show_error: bool = True,
213
399
  ) -> 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.
400
+ """Convert the request response (JSon) to a Python dict in a safe way.
401
+
402
+ This includes handling exceptions.
403
+
404
+ It first tries to load the response.text via json.loads() that produces
405
+ a dict output. Only if response.text is not set or is empty it just converts
406
+ the response_object to a dict using the vars() built-in method.
219
407
 
220
408
  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
409
+ response_object (object):
410
+ This is reponse object delivered by the request call.
411
+ additional_error_message (str, optional):
412
+ Provide a a more specific error message that is logged in case of an error.
413
+ show_error (bool):
414
+ If True, write an error to the log file.
415
+ If False, write a warning to the log file.
416
+
226
417
  Returns:
227
- dict: response information or None in case of an error
418
+ dict | None: Parsed response information or None in case of an error.
419
+
228
420
  """
229
421
 
230
422
  if not response_object:
231
423
  return None
232
424
 
233
425
  try:
234
- if response_object.text:
235
- dict_object = json.loads(response_object.text)
236
- else:
237
- dict_object = vars(response_object)
426
+ dict_object = json.loads(response_object.text) if response_object.text else vars(response_object)
238
427
  except json.JSONDecodeError as exception:
239
428
  if additional_error_message:
240
429
  message = "Cannot decode response as JSon. {}; error -> {}".format(
241
- additional_error_message, exception
430
+ additional_error_message,
431
+ exception,
242
432
  )
243
433
  else:
244
434
  message = "Cannot decode response as JSon; error -> {}".format(
245
- exception
435
+ exception,
246
436
  )
247
437
  if show_error:
248
- logger.error(message)
438
+ self.logger.error(message)
249
439
  else:
250
- logger.warning(message)
440
+ self.logger.warning(message)
251
441
  return None
252
442
  else:
253
443
  return dict_object
@@ -255,14 +445,20 @@ class Salesforce(object):
255
445
  # end method definition
256
446
 
257
447
  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.
448
+ """Check existence of key / value pair in the response properties of a Salesforce API call.
259
449
 
260
450
  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
451
+ response (dict):
452
+ REST response from an Salesforce API call
453
+ key (str):
454
+ The property name (key) of the item to lookup.
455
+ value (str):
456
+ The value to find in the item with the matching key.
457
+
264
458
  Returns:
265
- bool: True if the value was found, False otherwise
459
+ bool:
460
+ True if the value was found, False otherwise.
461
+
266
462
  """
267
463
 
268
464
  if not response:
@@ -274,10 +470,10 @@ class Salesforce(object):
274
470
  return False
275
471
 
276
472
  for record in records:
277
- if value == record[key]:
473
+ if key in record and value == record[key]:
278
474
  return True
279
475
  else:
280
- if not key in response:
476
+ if key not in response:
281
477
  return False
282
478
  if value == response[key]:
283
479
  return True
@@ -292,15 +488,22 @@ class Salesforce(object):
292
488
  key: str,
293
489
  index: int = 0,
294
490
  ) -> str | None:
295
- """Get value of a result property with a given key of an Salesforce API call.
491
+ """Get the value of a result property with a given key of an Salesforce API call.
296
492
 
297
493
  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.
494
+ response (dict):
495
+ REST response from an Salesforce REST Call.
496
+ key (str):
497
+ The property name (key) of the item to lookup.
498
+ index (int, optional):
499
+ Index to use (1st element has index 0).
500
+ Defaults to 0.
501
+
302
502
  Returns:
303
- str: value for the key, None otherwise
503
+ str | None:
504
+ The value for the key or None in case of an error or if the
505
+ key is not found.
506
+
304
507
  """
305
508
 
306
509
  if not response:
@@ -314,7 +517,7 @@ class Salesforce(object):
314
517
  return None
315
518
  value = values[index][key]
316
519
  else: # simple response - try to find key in response directly:
317
- if not key in response:
520
+ if key not in response:
318
521
  return None
319
522
  value = response[key]
320
523
 
@@ -326,15 +529,20 @@ class Salesforce(object):
326
529
  """Authenticate at Salesforce with client ID and client secret.
327
530
 
328
531
  Args:
329
- revalidate (bool, optional): determinse if a re-athentication is enforced
330
- (e.g. if session has timed out with 401 error)
532
+ revalidate (bool, optional):
533
+ Determins if a re-athentication is enforced
534
+ (e.g. if session has timed out with 401 error).
535
+
331
536
  Returns:
332
- str: Access token. Also stores access token in self._access_token. None in case of error
537
+ str | None:
538
+ The Access token. Also stores access token in self._access_token.
539
+ None in case of error
540
+
333
541
  """
334
542
 
335
543
  # Already authenticated and session still valid?
336
544
  if self._access_token and not revalidate:
337
- logger.debug(
545
+ self.logger.debug(
338
546
  "Session still valid - return existing access token -> %s",
339
547
  str(self._access_token),
340
548
  )
@@ -343,7 +551,7 @@ class Salesforce(object):
343
551
  request_url = self.config()["authenticationUrl"]
344
552
  request_header = REQUEST_LOGIN_HEADERS
345
553
 
346
- logger.debug("Requesting Salesforce Access Token from -> %s", request_url)
554
+ self.logger.debug("Requesting Salesforce Access Token from -> %s", request_url)
347
555
 
348
556
  authenticate_post_body = self.credentials()
349
557
 
@@ -359,7 +567,7 @@ class Salesforce(object):
359
567
  timeout=REQUEST_TIMEOUT,
360
568
  )
361
569
  except requests.exceptions.ConnectionError as exception:
362
- logger.warning(
570
+ self.logger.warning(
363
571
  "Unable to connect to -> %s : %s",
364
572
  self.config()["authenticationUrl"],
365
573
  exception,
@@ -373,11 +581,11 @@ class Salesforce(object):
373
581
  else:
374
582
  # Store authentication access_token:
375
583
  self._access_token = authenticate_dict["access_token"]
376
- logger.debug("Access Token -> %s", self._access_token)
584
+ self.logger.debug("Access Token -> %s", self._access_token)
377
585
  self._instance_url = authenticate_dict["instance_url"]
378
- logger.debug("Instance URL -> %s", self._instance_url)
586
+ self.logger.debug("Instance URL -> %s", self._instance_url)
379
587
  else:
380
- logger.error(
588
+ self.logger.error(
381
589
  "Failed to request an Salesforce Access Token; error -> %s",
382
590
  response.text,
383
591
  )
@@ -388,8 +596,11 @@ class Salesforce(object):
388
596
  # end method definition
389
597
 
390
598
  def get_object_id_by_name(
391
- self, object_type: str, name: str, name_field: str = "Name"
392
- ) -> Optional[str]:
599
+ self,
600
+ object_type: str,
601
+ name: str,
602
+ name_field: str = "Name",
603
+ ) -> str | None:
393
604
  """Get the ID of a given Salesforce object with a given type and name.
394
605
 
395
606
  Args:
@@ -399,6 +610,7 @@ class Salesforce(object):
399
610
 
400
611
  Returns:
401
612
  Optional[str]: Object ID or None if the request fails.
613
+
402
614
  """
403
615
 
404
616
  if not self._access_token or not self._instance_url:
@@ -409,32 +621,21 @@ class Salesforce(object):
409
621
 
410
622
  query = f"SELECT Id FROM {object_type} WHERE {name_field} = '{name}'"
411
623
 
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
624
+ response = self.do_request(
625
+ method="GET",
626
+ url=request_url,
627
+ headers=request_header,
628
+ params={"q": query},
629
+ timeout=REQUEST_TIMEOUT,
630
+ failure_message="Failed to get Salesforce object ID for object type -> '{}' and object name -> '{}'".format(
631
+ object_type,
632
+ name,
633
+ ),
634
+ )
635
+ if not response:
636
+ return None
637
+
638
+ return self.get_result_value(response, "Id")
438
639
 
439
640
  # end method definition
440
641
 
@@ -450,16 +651,18 @@ class Salesforce(object):
450
651
 
451
652
  Args:
452
653
  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.
654
+ search_field (str): The object field to search in.
655
+ search_value (str): The value to search for.
656
+ result_fields (list | None):
657
+ List of fields to return. If None, then all standard fields
658
+ of the object will be returned.
659
+ limit (int, optional):
660
+ The maximum number of fields to return. Salesforce enforces 200 as upper limit.
458
661
 
459
662
  Returns:
460
663
  dict | None: Dictionary with the Salesforce object data.
461
664
 
462
- Example response:
665
+ Example:
463
666
  {
464
667
  'totalSize': 2,
465
668
  'done': True,
@@ -480,20 +683,21 @@ class Salesforce(object):
480
683
  }
481
684
  ]
482
685
  }
686
+
483
687
  """
484
688
 
485
689
  if not self._access_token or not self._instance_url:
486
690
  self.authenticate()
487
691
 
488
692
  if search_field and not search_value:
489
- logger.error(
693
+ self.logger.error(
490
694
  "No search value has been provided for search field -> %s!",
491
695
  search_field,
492
696
  )
493
697
  return None
494
698
  if not result_fields:
495
- logger.debug(
496
- "No result fields defined. Using 'FIELDS(STANDARD)' to deliver all standard fields of the object."
699
+ self.logger.debug(
700
+ "No result fields defined. Using 'FIELDS(STANDARD)' to deliver all standard fields of the object.",
497
701
  )
498
702
  result_fields = ["FIELDS(STANDARD)"]
499
703
 
@@ -505,45 +709,41 @@ class Salesforce(object):
505
709
  request_header = self.request_header()
506
710
  request_url = self.config()["queryUrl"] + "?q={}".format(query)
507
711
 
508
- logger.debug(
509
- "Sending query -> %s to Salesforce; calling -> %s", query, request_url
712
+ self.logger.debug(
713
+ "Sending query -> %s to Salesforce; calling -> %s",
714
+ query,
715
+ request_url,
510
716
  )
511
717
 
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
718
+ return self.do_request(
719
+ method="GET",
720
+ url=request_url,
721
+ headers=request_header,
722
+ timeout=REQUEST_TIMEOUT,
723
+ failure_message="Failed to retrieve Salesforce object type -> '{}' with {} = {}".format(
724
+ object_type,
725
+ search_field,
726
+ search_value,
727
+ ),
728
+ )
534
729
 
535
730
  # end method definition
536
731
 
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.
732
+ def add_object(self, object_type: str, **kwargs: dict[str, str]) -> dict | None:
733
+ """Add object to Salesforce.
734
+
735
+ This is a generic wrapper method for the actual add methods.
540
736
 
541
737
  Args:
542
- object_type (str): Type of the Salesforce business object, like "Account" or "Case".
543
- **kwargs (dict): keyword / value ictionary with additional parameters
738
+ object_type (str):
739
+ Type of the Salesforce business object, like "Account" or "Case".
740
+ **kwargs (dict):
741
+ This is a keyword / value dictionary with additional parameters that depend
742
+ on the object type.
544
743
 
545
744
  Returns:
546
745
  dict | None: Dictionary with the Salesforce object data or None if the request fails.
746
+
547
747
  """
548
748
 
549
749
  match object_type:
@@ -611,25 +811,30 @@ class Salesforce(object):
611
811
  **kwargs,
612
812
  )
613
813
  case _:
614
- logger.error(
814
+ self.logger.error(
615
815
  "Unsupported Salesforce business object -> %s!",
616
816
  object_type,
617
817
  )
618
818
 
819
+ return None
820
+
619
821
  # end method definition
620
822
 
621
- def get_group_id(self, groupname: str) -> Optional[str]:
823
+ def get_group_id(self, group_name: str) -> str | None:
622
824
  """Get a group ID by group name.
623
825
 
624
826
  Args:
625
- groupname (str): Name of the Group.
827
+ group_name (str): Name of the Group.
626
828
 
627
829
  Returns:
628
830
  Optional[str]: Technical ID of the group
831
+
629
832
  """
630
833
 
631
834
  return self.get_object_id_by_name(
632
- object_type="Group", name=groupname, name_field="Name"
835
+ object_type="Group",
836
+ name=group_name,
837
+ name_field="Name",
633
838
  )
634
839
 
635
840
  # end method definition
@@ -638,10 +843,13 @@ class Salesforce(object):
638
843
  """Get a Salesforce group based on its ID.
639
844
 
640
845
  Args:
641
- group_id (str): ID of the Salesforce group
846
+ group_id (str):
847
+ ID of the Salesforce group to retrieve.
642
848
 
643
849
  Returns:
644
- dict | None: Dictionary with the Salesforce group data or None if the request fails.
850
+ dict | None:
851
+ Dictionary with the Salesforce group data or None if the request fails.
852
+
645
853
  """
646
854
 
647
855
  if not self._access_token or not self._instance_url:
@@ -650,30 +858,21 @@ class Salesforce(object):
650
858
  request_header = self.request_header()
651
859
  request_url = self.config()["groupUrl"] + group_id
652
860
 
653
- logger.debug(
654
- "Get Salesforce group with ID -> %s; calling -> %s", group_id, request_url
861
+ self.logger.debug(
862
+ "Get Salesforce group with ID -> %s; calling -> %s",
863
+ group_id,
864
+ request_url,
655
865
  )
656
866
 
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
867
+ return self.do_request(
868
+ method="GET",
869
+ url=request_url,
870
+ headers=request_header,
871
+ timeout=REQUEST_TIMEOUT,
872
+ failure_message="Failed to get Salesforce group with ID -> {}".format(
873
+ group_id,
874
+ ),
875
+ )
677
876
 
678
877
  # end method definition
679
878
 
@@ -686,16 +885,18 @@ class Salesforce(object):
686
885
 
687
886
  Args:
688
887
  group_name (str): Name of the new Salesforce group
888
+ group_type (str, optional): Type of the group. Default is "Regular".
689
889
 
690
890
  Returns:
691
891
  dict | None: Dictionary with the Salesforce Group data or None if the request fails.
692
892
 
693
- Example response:
893
+ Example:
694
894
  {
695
895
  'id': '00GDn000000KWE0MAO',
696
896
  'success': True,
697
897
  'errors': []
698
898
  }
899
+
699
900
  """
700
901
 
701
902
  if not self._access_token or not self._instance_url:
@@ -706,33 +907,20 @@ class Salesforce(object):
706
907
 
707
908
  payload = {"Name": group_name, "Type": group_type}
708
909
 
709
- logger.debug(
710
- "Adding Salesforce group -> %s; calling -> %s", group_name, request_url
910
+ self.logger.debug(
911
+ "Adding Salesforce group -> %s; calling -> %s",
912
+ group_name,
913
+ request_url,
711
914
  )
712
915
 
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
916
+ return self.do_request(
917
+ method="POST",
918
+ url=request_url,
919
+ headers=request_header,
920
+ data=json.dumps(payload),
921
+ timeout=REQUEST_TIMEOUT,
922
+ failure_message="Failed to add Salesforce group -> '{}'".format(group_name),
923
+ )
736
924
 
737
925
  # end method definition
738
926
 
@@ -740,7 +928,7 @@ class Salesforce(object):
740
928
  self,
741
929
  group_id: str,
742
930
  update_data: dict,
743
- ) -> dict:
931
+ ) -> dict | None:
744
932
  """Update a Salesforce group.
745
933
 
746
934
  Args:
@@ -748,7 +936,8 @@ class Salesforce(object):
748
936
  update_data (dict): Dictionary containing the fields to update.
749
937
 
750
938
  Returns:
751
- dict: Response from the Salesforce API.
939
+ dict | None: Response from the Salesforce API. None in case of an error.
940
+
752
941
  """
753
942
 
754
943
  if not self._access_token or not self._instance_url:
@@ -758,48 +947,37 @@ class Salesforce(object):
758
947
 
759
948
  request_url = self.config()["groupUrl"] + group_id
760
949
 
761
- logger.debug(
950
+ self.logger.debug(
762
951
  "Update Salesforce group with ID -> %s; calling -> %s",
763
952
  group_id,
764
953
  request_url,
765
954
  )
766
955
 
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
956
+ return self.do_request(
957
+ method="PATCH",
958
+ url=request_url,
959
+ headers=request_header,
960
+ json_data=update_data,
961
+ timeout=REQUEST_TIMEOUT,
962
+ failure_message="Failed to update Salesforce group with ID -> {}".format(
963
+ group_id,
964
+ ),
965
+ )
790
966
 
791
967
  # end method definition
792
968
 
793
969
  def get_group_members(self, group_id: str) -> list | None:
794
- """Get Salesforce group members
970
+ """Get Salesforce group members.
795
971
 
796
972
  Args:
797
- group_id (str): Id of the group to retrieve the members
973
+ group_id (str):
974
+ The ID of the group to retrieve the members.
798
975
 
799
976
  Returns:
800
- list | None: result
977
+ list | None:
978
+ The group members.
801
979
 
802
- Example response:
980
+ Example:
803
981
  {
804
982
  'totalSize': 1,
805
983
  'done': True,
@@ -813,6 +991,7 @@ class Salesforce(object):
813
991
  }
814
992
  ]
815
993
  }
994
+
816
995
  """
817
996
 
818
997
  if not self._access_token or not self._instance_url:
@@ -825,54 +1004,45 @@ class Salesforce(object):
825
1004
  query = f"SELECT UserOrGroupId FROM GroupMember WHERE GroupId = '{group_id}'"
826
1005
  params = {"q": query}
827
1006
 
828
- logger.debug(
1007
+ self.logger.debug(
829
1008
  "Get members of Salesforce group with ID -> %s; calling -> %s",
830
1009
  group_id,
831
1010
  request_url,
832
1011
  )
833
1012
 
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
1013
+ return self.do_request(
1014
+ method="GET",
1015
+ url=request_url,
1016
+ headers=request_header,
1017
+ params=params,
1018
+ timeout=REQUEST_TIMEOUT,
1019
+ failure_message="Failed to get members of Salesforce group with ID -> {}".format(
1020
+ group_id,
1021
+ ),
1022
+ )
857
1023
 
858
1024
  # end method definition
859
1025
 
860
1026
  def add_group_member(self, group_id: str, member_id: str) -> dict | None:
861
- """Add a user or group to a Salesforce group
1027
+ """Add a user or group to a Salesforce group.
862
1028
 
863
1029
  Args:
864
- group_id (str): ID of the Salesforce Group to add member to.
865
- member_id (str): ID of the user or group.
1030
+ group_id (str):
1031
+ The ID of the Salesforce Group to add member to.
1032
+ member_id (str):
1033
+ The ID of the user or group.
866
1034
 
867
1035
  Returns:
868
- dict | None: Dictionary with the Salesforce membership data or None if the request fails.
1036
+ dict | None:
1037
+ Dictionary with the Salesforce membership data or None if the request fails.
869
1038
 
870
- Example response (id is the membership ID):
1039
+ Example response (id is the membership ID):
871
1040
  {
872
1041
  'id': '011Dn000000ELhwIAG',
873
1042
  'success': True,
874
1043
  'errors': []
875
1044
  }
1045
+
876
1046
  """
877
1047
 
878
1048
  if not self._access_token or not self._instance_url:
@@ -884,46 +1054,35 @@ class Salesforce(object):
884
1054
 
885
1055
  payload = {"GroupId": group_id, "UserOrGroupId": member_id}
886
1056
 
887
- logger.debug(
1057
+ self.logger.debug(
888
1058
  "Add member with ID -> %s to Salesforce group with ID -> %s; calling -> %s",
889
1059
  member_id,
890
1060
  group_id,
891
1061
  request_url,
892
1062
  )
893
1063
 
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
1064
+ return self.do_request(
1065
+ method="POST",
1066
+ url=request_url,
1067
+ headers=request_header,
1068
+ json_data=payload,
1069
+ timeout=REQUEST_TIMEOUT,
1070
+ failure_message="Failed to add member with ID -> {} to Salesforce group with ID -> {}".format(
1071
+ member_id,
1072
+ group_id,
1073
+ ),
1074
+ )
917
1075
 
918
1076
  # end method definition
919
1077
 
920
1078
  def get_all_user_profiles(self) -> dict | None:
921
- """Get all user profiles
1079
+ """Get all user profiles.
922
1080
 
923
1081
  Returns:
924
- dict | None: Dictionary with salesforce user profiles.
1082
+ dict | None:
1083
+ Dictionary with Salesforce user profiles.
925
1084
 
926
- Example response:
1085
+ Example response:
927
1086
  {
928
1087
  'totalSize': 15,
929
1088
  'done': True,
@@ -935,8 +1094,7 @@ class Salesforce(object):
935
1094
  'url': '/services/data/v52.0/sobjects/Profile/00eDn000001msL8IAI'},
936
1095
  'Id': '00eDn000001msL8IAI',
937
1096
  'Name': 'Standard User',
938
- 'CreatedById':
939
- '005Dn000001rRodIAE',
1097
+ 'CreatedById': '005Dn000001rRodIAE',
940
1098
  'CreatedDate': '2022-11-30T15:30:54.000+0000',
941
1099
  'Description': None,
942
1100
  'LastModifiedById': '005Dn000001rUacIAE',
@@ -948,6 +1106,7 @@ class Salesforce(object):
948
1106
  }, ...
949
1107
  ]
950
1108
  }
1109
+
951
1110
  """
952
1111
 
953
1112
  if not self._access_token or not self._instance_url:
@@ -958,32 +1117,18 @@ class Salesforce(object):
958
1117
 
959
1118
  query = "SELECT Id, Name, CreatedById, CreatedDate, Description, LastModifiedById, LastModifiedDate, PermissionsCustomizeApplication, PermissionsEditTask, PermissionsImportLeads FROM Profile"
960
1119
 
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
1120
+ return self.do_request(
1121
+ method="GET",
1122
+ url=request_url,
1123
+ headers=request_header,
1124
+ params={"q": query},
1125
+ timeout=REQUEST_TIMEOUT,
1126
+ failure_message="Failed to get Salesforce user profiles",
1127
+ )
983
1128
 
984
1129
  # end method definition
985
1130
 
986
- def get_user_profile_id(self, profile_name: str) -> Optional[str]:
1131
+ def get_user_profile_id(self, profile_name: str) -> str | None:
987
1132
  """Get a user profile ID by profile name.
988
1133
 
989
1134
  Args:
@@ -991,13 +1136,14 @@ class Salesforce(object):
991
1136
 
992
1137
  Returns:
993
1138
  Optional[str]: Technical ID of the user profile.
1139
+
994
1140
  """
995
1141
 
996
1142
  return self.get_object_id_by_name(object_type="Profile", name=profile_name)
997
1143
 
998
1144
  # end method definition
999
1145
 
1000
- def get_user_id(self, username: str) -> Optional[str]:
1146
+ def get_user_id(self, username: str) -> str | None:
1001
1147
  """Get a user ID by user name.
1002
1148
 
1003
1149
  Args:
@@ -1005,10 +1151,13 @@ class Salesforce(object):
1005
1151
 
1006
1152
  Returns:
1007
1153
  Optional[str]: Technical ID of the user
1154
+
1008
1155
  """
1009
1156
 
1010
1157
  return self.get_object_id_by_name(
1011
- object_type="User", name=username, name_field="Username"
1158
+ object_type="User",
1159
+ name=username,
1160
+ name_field="Username",
1012
1161
  )
1013
1162
 
1014
1163
  # end method definition
@@ -1021,6 +1170,7 @@ class Salesforce(object):
1021
1170
 
1022
1171
  Returns:
1023
1172
  dict | None: Dictionary with the Salesforce user data or None if the request fails.
1173
+
1024
1174
  """
1025
1175
 
1026
1176
  if not self._access_token or not self._instance_url:
@@ -1029,30 +1179,21 @@ class Salesforce(object):
1029
1179
  request_header = self.request_header()
1030
1180
  request_url = self.config()["userUrl"] + user_id
1031
1181
 
1032
- logger.debug(
1033
- "Get Salesforce user with ID -> %s; calling -> %s", user_id, request_url
1182
+ self.logger.debug(
1183
+ "Get Salesforce user with ID -> %s; calling -> %s",
1184
+ user_id,
1185
+ request_url,
1034
1186
  )
1035
1187
 
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
1188
+ return self.do_request(
1189
+ method="GET",
1190
+ url=request_url,
1191
+ headers=request_header,
1192
+ timeout=REQUEST_TIMEOUT,
1193
+ failure_message="Failed to get Salesforce user with ID -> {}".format(
1194
+ user_id,
1195
+ ),
1196
+ )
1056
1197
 
1057
1198
  # end method definition
1058
1199
 
@@ -1065,34 +1206,48 @@ class Salesforce(object):
1065
1206
  title: str | None = None,
1066
1207
  department: str | None = None,
1067
1208
  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,
1209
+ profile_name: str | None = "Standard User",
1210
+ profile_id: str | None = None,
1211
+ time_zone_key: str | None = "America/Los_Angeles",
1212
+ email_encoding_key: str | None = "ISO-8859-1",
1213
+ locale_key: str | None = "en_US",
1214
+ alias: str | None = None,
1074
1215
  ) -> dict | None:
1075
1216
  """Add a new Salesforce user. The password has to be set separately.
1076
1217
 
1077
1218
  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.
1219
+ username (str):
1220
+ Login name of the new user
1221
+ email (str):
1222
+ Email of the new user
1223
+ firstname (str):
1224
+ First name of the new user.
1225
+ lastname (str):
1226
+ Last name of the new user.
1227
+ title (str):
1228
+ Title of the user.
1229
+ department (str):
1230
+ Department of the user.
1231
+ company_name (str):
1232
+ Name of the Company of the user.
1233
+ profile_name (str):
1234
+ Profile name like "Standard User"
1235
+ profile_id (str, optional):
1236
+ Profile ID of the new user. Defaults to None.
1237
+ Use method get_all_user_profiles() to determine
1238
+ the desired Profile for the user. Or pass the profile_name.
1239
+ time_zone_key (str, optional):
1240
+ Timezone provided in format country/city like "America/Los_Angeles",
1241
+ email_encoding_key (str, optional):
1242
+ Default is "ISO-8859-1".
1243
+ locale_key (str, optional):
1244
+ Default is "en_US".
1245
+ alias (str, optional):
1246
+ Alias of the new user. Defaults to None.
1093
1247
 
1094
1248
  Returns:
1095
1249
  dict | None: Dictionary with the Salesforce User data or None if the request fails.
1250
+
1096
1251
  """
1097
1252
 
1098
1253
  if not self._access_token or not self._instance_url:
@@ -1121,33 +1276,20 @@ class Salesforce(object):
1121
1276
  "LanguageLocaleKey": locale_key, # Set default LanguageLocaleKey
1122
1277
  }
1123
1278
 
1124
- logger.debug(
1125
- "Adding Salesforce user -> %s; calling -> %s", username, request_url
1279
+ self.logger.debug(
1280
+ "Adding Salesforce user -> %s; calling -> %s",
1281
+ username,
1282
+ request_url,
1126
1283
  )
1127
1284
 
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
1285
+ return self.do_request(
1286
+ method="POST",
1287
+ url=request_url,
1288
+ headers=request_header,
1289
+ data=json.dumps(payload),
1290
+ timeout=REQUEST_TIMEOUT,
1291
+ failure_message="Failed to add Salesforce user -> {}".format(username),
1292
+ )
1151
1293
 
1152
1294
  # end method definition
1153
1295
 
@@ -1164,6 +1306,7 @@ class Salesforce(object):
1164
1306
 
1165
1307
  Returns:
1166
1308
  dict: Response from the Salesforce API.
1309
+
1167
1310
  """
1168
1311
 
1169
1312
  if not self._access_token or not self._instance_url:
@@ -1173,33 +1316,22 @@ class Salesforce(object):
1173
1316
 
1174
1317
  request_url = self.config()["userUrl"] + user_id
1175
1318
 
1176
- logger.debug(
1177
- "Update Salesforce user with ID -> %s; calling -> %s", user_id, request_url
1319
+ self.logger.debug(
1320
+ "Update Salesforce user with ID -> %s; calling -> %s",
1321
+ user_id,
1322
+ request_url,
1178
1323
  )
1179
1324
 
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
1325
+ return self.do_request(
1326
+ method="PATCH",
1327
+ url=request_url,
1328
+ headers=request_header,
1329
+ json_data=update_data,
1330
+ timeout=REQUEST_TIMEOUT,
1331
+ failure_message="Failed to update Salesforce user with ID -> {}".format(
1332
+ user_id,
1333
+ ),
1334
+ )
1203
1335
 
1204
1336
  # end method definition
1205
1337
 
@@ -1207,7 +1339,7 @@ class Salesforce(object):
1207
1339
  self,
1208
1340
  user_id: str,
1209
1341
  password: str,
1210
- ) -> dict:
1342
+ ) -> dict | None:
1211
1343
  """Update the password of a Salesforce user.
1212
1344
 
1213
1345
  Args:
@@ -1216,6 +1348,7 @@ class Salesforce(object):
1216
1348
 
1217
1349
  Returns:
1218
1350
  dict: Response from the Salesforce API.
1351
+
1219
1352
  """
1220
1353
 
1221
1354
  if not self._access_token or not self._instance_url:
@@ -1225,7 +1358,7 @@ class Salesforce(object):
1225
1358
 
1226
1359
  request_url = self.config()["userUrl"] + "{}/password".format(user_id)
1227
1360
 
1228
- logger.debug(
1361
+ self.logger.debug(
1229
1362
  "Update password of Salesforce user with ID -> %s; calling -> %s",
1230
1363
  user_id,
1231
1364
  request_url,
@@ -1233,29 +1366,16 @@ class Salesforce(object):
1233
1366
 
1234
1367
  update_data = {"NewPassword": password}
1235
1368
 
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
1369
+ return self.do_request(
1370
+ method="POST",
1371
+ url=request_url,
1372
+ headers=request_header,
1373
+ json_data=update_data,
1374
+ timeout=REQUEST_TIMEOUT,
1375
+ failure_message="Failed to update password of Salesforce user with ID -> {}".format(
1376
+ user_id,
1377
+ ),
1378
+ )
1259
1379
 
1260
1380
  # end method definition
1261
1381
 
@@ -1269,8 +1389,10 @@ class Salesforce(object):
1269
1389
  Args:
1270
1390
  user_id (str): Salesforce ID of the user
1271
1391
  photo_path (str): file system path with the location of the photo
1392
+
1272
1393
  Returns:
1273
1394
  dict | None: Dictionary with the Salesforce User data or None if the request fails.
1395
+
1274
1396
  """
1275
1397
 
1276
1398
  if not self._access_token or not self._instance_url:
@@ -1278,20 +1400,23 @@ class Salesforce(object):
1278
1400
 
1279
1401
  # Check if the photo file exists
1280
1402
  if not os.path.isfile(photo_path):
1281
- logger.error("Photo file -> %s not found!", photo_path)
1403
+ self.logger.error("Photo file -> %s not found!", photo_path)
1282
1404
  return None
1283
1405
 
1284
1406
  try:
1285
1407
  # Read the photo file as binary data
1286
1408
  with open(photo_path, "rb") as image_file:
1287
1409
  photo_data = image_file.read()
1288
- except OSError as exception:
1410
+ except OSError:
1289
1411
  # Handle any errors that occurred while reading the photo file
1290
- logger.error(
1291
- "Error reading photo file -> %s; error -> %s", photo_path, exception
1412
+ self.logger.error(
1413
+ "Error reading photo file -> %s",
1414
+ photo_path,
1292
1415
  )
1293
1416
  return None
1294
1417
 
1418
+ # Content Type = None is important as upload calls need
1419
+ # a multipart header that is automatically selected if None is used:
1295
1420
  request_header = self.request_header(content_type=None)
1296
1421
 
1297
1422
  data = {"json": json.dumps({"cropX": 0, "cropY": 0, "cropSize": 200})}
@@ -1301,40 +1426,27 @@ class Salesforce(object):
1301
1426
  photo_path,
1302
1427
  photo_data,
1303
1428
  "application/octet-stream",
1304
- )
1429
+ ),
1305
1430
  }
1306
1431
 
1307
- logger.debug(
1432
+ self.logger.debug(
1308
1433
  "Update profile photo of Salesforce user with ID -> %s; calling -> %s",
1309
1434
  user_id,
1310
1435
  request_url,
1311
1436
  )
1312
1437
 
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
1438
+ return self.do_request(
1439
+ method="POST",
1440
+ url=request_url,
1441
+ headers=request_header,
1442
+ files=files,
1443
+ data=data,
1444
+ timeout=REQUEST_TIMEOUT,
1445
+ failure_message="Failed to update profile photo of Salesforce user with ID -> {}".format(
1446
+ user_id,
1447
+ ),
1448
+ verify=False,
1449
+ )
1338
1450
 
1339
1451
  # end method definition
1340
1452
 
@@ -1343,11 +1455,11 @@ class Salesforce(object):
1343
1455
  account_name: str,
1344
1456
  account_number: str,
1345
1457
  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,
1458
+ description: str | None = None,
1459
+ industry: str | None = None,
1460
+ website: str | None = None,
1461
+ phone: str | None = None,
1462
+ **kwargs: dict[str, str],
1351
1463
  ) -> dict | None:
1352
1464
  """Add a new Account object to Salesforce.
1353
1465
 
@@ -1359,10 +1471,11 @@ class Salesforce(object):
1359
1471
  industry (str, optional): Industry of the new Salesforce account. Defaults to None.
1360
1472
  website (str, optional): Website of the new Salesforce account. Defaults to None.
1361
1473
  phone (str, optional): Phone number of the new Salesforce account. Defaults to None.
1362
- kwargs (Any): Additional values (e.g. custom fields)
1474
+ kwargs (dict): Additional values (e.g. custom fields)
1363
1475
 
1364
1476
  Returns:
1365
1477
  dict | None: Dictionary with the Salesforce Account data or None if the request fails.
1478
+
1366
1479
  """
1367
1480
 
1368
1481
  if not self._access_token or not self._instance_url:
@@ -1382,33 +1495,24 @@ class Salesforce(object):
1382
1495
  }
1383
1496
  payload.update(kwargs) # Add additional fields from kwargs
1384
1497
 
1385
- logger.debug(
1386
- "Adding Salesforce account -> %s; calling -> %s", account_name, request_url
1498
+ self.logger.debug(
1499
+ "Adding Salesforce account -> '%s' (%s); calling -> %s",
1500
+ account_name,
1501
+ account_number,
1502
+ request_url,
1387
1503
  )
1388
1504
 
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
1505
+ return self.do_request(
1506
+ method="POST",
1507
+ url=request_url,
1508
+ headers=request_header,
1509
+ data=json.dumps(payload),
1510
+ timeout=REQUEST_TIMEOUT,
1511
+ failure_message="Failed to add Salesforce account -> '{}' ({})".format(
1512
+ account_name,
1513
+ account_number,
1514
+ ),
1515
+ )
1412
1516
 
1413
1517
  # end method definition
1414
1518
 
@@ -1418,18 +1522,25 @@ class Salesforce(object):
1418
1522
  product_code: str,
1419
1523
  description: str,
1420
1524
  price: float,
1421
- **kwargs: Any,
1525
+ **kwargs: dict[str, str],
1422
1526
  ) -> dict | None:
1423
1527
  """Add a new Product object to Salesforce.
1424
1528
 
1425
1529
  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.
1530
+ product_name (str):
1531
+ Name of the Salesforce Product.
1532
+ product_code (str):
1533
+ Code of the Salesforce Product.
1534
+ description (str):
1535
+ Description of the Salesforce Product.
1536
+ price (float):
1537
+ Price of the Salesforce Product.
1538
+ kwargs:
1539
+ Additional keyword arguments.
1430
1540
 
1431
1541
  Returns:
1432
1542
  dict | None: Dictionary with the Salesforce Product data or None if the request fails.
1543
+
1433
1544
  """
1434
1545
 
1435
1546
  if not self._access_token or not self._instance_url:
@@ -1446,33 +1557,24 @@ class Salesforce(object):
1446
1557
  }
1447
1558
  payload.update(kwargs) # Add additional fields from kwargs
1448
1559
 
1449
- logger.debug(
1450
- "Add Salesforce product -> %s; calling -> %s", product_name, request_url
1560
+ self.logger.debug(
1561
+ "Add Salesforce product -> '%s' (%s); calling -> %s",
1562
+ product_name,
1563
+ product_code,
1564
+ request_url,
1451
1565
  )
1452
1566
 
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
1567
+ return self.do_request(
1568
+ method="POST",
1569
+ url=request_url,
1570
+ headers=request_header,
1571
+ data=json.dumps(payload),
1572
+ timeout=REQUEST_TIMEOUT,
1573
+ failure_message="Failed to add Salesforce product -> '{}' ({})".format(
1574
+ product_name,
1575
+ product_code,
1576
+ ),
1577
+ )
1476
1578
 
1477
1579
  # end method definition
1478
1580
 
@@ -1481,25 +1583,38 @@ class Salesforce(object):
1481
1583
  name: str,
1482
1584
  stage: str,
1483
1585
  close_date: str,
1484
- amount: Union[int, float],
1586
+ amount: float,
1485
1587
  account_id: str,
1486
- description: str = None,
1487
- **kwargs: Any,
1588
+ description: str | None = None,
1589
+ **kwargs: dict[str, str],
1488
1590
  ) -> dict | None:
1489
1591
  """Add a new Opportunity object to Salesfoce.
1490
1592
 
1491
1593
  Args:
1492
1594
  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.
1595
+ stage (str):
1596
+ Stage of the Opportunity. Typical Value:
1597
+ - "Prospecting"
1598
+ - "Qualification"
1599
+ - "Value Proposition"
1600
+ - "Negotiation/Review",
1601
+ - "Closed Won"
1602
+ - "Closed Lost"
1603
+ close_date (str):
1604
+ Close date of the Opportunity. Should be in format YYYY-MM-DD.
1605
+ amount (Union[int, float]):
1606
+ Amount (expected revenue) of the opportunity.
1607
+ Can either be an integer or a float value.
1608
+ account_id (str):
1609
+ The technical ID of the related Salesforce Account.
1610
+ description (str | None, optional):
1611
+ A description of the opportunity.
1612
+ kwargs (dict):
1613
+ Additional keyword arguments.
1500
1614
 
1501
1615
  Returns:
1502
1616
  dict | None: Dictionary with the Salesforce Opportunity data or None if the request fails.
1617
+
1503
1618
  """
1504
1619
 
1505
1620
  if not self._access_token or not self._instance_url:
@@ -1519,33 +1634,20 @@ class Salesforce(object):
1519
1634
  payload["Description"] = description
1520
1635
  payload.update(kwargs) # Add additional fields from kwargs
1521
1636
 
1522
- logger.debug(
1523
- "Add Salesforce opportunity -> %s; calling -> %s", name, request_url
1637
+ self.logger.debug(
1638
+ "Add Salesforce opportunity -> '%s'; calling -> %s",
1639
+ name,
1640
+ request_url,
1524
1641
  )
1525
1642
 
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
1643
+ return self.do_request(
1644
+ method="POST",
1645
+ url=request_url,
1646
+ headers=request_header,
1647
+ data=json.dumps(payload),
1648
+ timeout=REQUEST_TIMEOUT,
1649
+ failure_message="Failed to add Salesforce opportunity -> '{}'".format(name),
1650
+ )
1549
1651
 
1550
1652
  # end method definition
1551
1653
 
@@ -1558,27 +1660,40 @@ class Salesforce(object):
1558
1660
  origin: str,
1559
1661
  account_id: str,
1560
1662
  owner_id: str,
1561
- asset_id: Optional[str] = None,
1562
- product_id: Optional[str] = None,
1563
- **kwargs: Any,
1663
+ asset_id: str | None = None,
1664
+ product_id: str | None = None,
1665
+ **kwargs: dict[str, str],
1564
1666
  ) -> dict | None:
1565
- """Add a new Case object to Salesforce. The case number is automatically created and can not be
1566
- provided.
1667
+ """Add a new Case object to Salesforce.
1668
+
1669
+ The case number is automatically created and can not be provided.
1567
1670
 
1568
1671
  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)
1672
+ subject (str):
1673
+ Subject (title) of the case. It's like the name.
1674
+ description (str):
1675
+ Description of the case
1676
+ status (str):
1677
+ Status of the case. Typecal values: "New", "On Hold", "Escalated"
1678
+ priority (str):
1679
+ Priority of the case. Typical values: "High", "Medium", "Low".
1680
+ origin (str):
1681
+ Origin (source) of the case. Typical values: "Email", "Phone", "Web"
1682
+ account_id (str):
1683
+ Technical ID of the related Account
1684
+ owner_id (str):
1685
+ Owner of the case
1686
+ asset_id (str):
1687
+ Technical ID of the related Asset.
1688
+ product_id (str):
1689
+ Technical ID of the related Product.
1690
+ kwargs (dict):
1691
+ Additional values (e.g. custom fields)
1579
1692
 
1580
1693
  Returns:
1581
- dict | None: Dictionary with the Salesforce Case data or None if the request fails.
1694
+ dict | None:
1695
+ Dictionary with the Salesforce Case data or None if the request fails.
1696
+
1582
1697
  """
1583
1698
 
1584
1699
  if not self._access_token or not self._instance_url:
@@ -1603,31 +1718,20 @@ class Salesforce(object):
1603
1718
  payload["ProductId"] = product_id
1604
1719
  payload.update(kwargs) # Add additional fields from kwargs
1605
1720
 
1606
- logger.debug("Add Salesforce case -> %s; calling -> %s", subject, request_url)
1721
+ self.logger.debug(
1722
+ "Add Salesforce case -> '%s'; calling -> %s",
1723
+ subject,
1724
+ request_url,
1725
+ )
1607
1726
 
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
1727
+ return self.do_request(
1728
+ method="POST",
1729
+ url=request_url,
1730
+ headers=request_header,
1731
+ data=json.dumps(payload),
1732
+ timeout=REQUEST_TIMEOUT,
1733
+ failure_message="Failed to add Salesforce case -> '{}'".format(subject),
1734
+ )
1631
1735
 
1632
1736
  # end method definition
1633
1737
 
@@ -1640,22 +1744,33 @@ class Salesforce(object):
1640
1744
  purchase_date: str,
1641
1745
  install_date: str,
1642
1746
  description: str | None = None,
1643
- **kwargs: Any,
1747
+ **kwargs: dict[str, str],
1644
1748
  ) -> dict | None:
1645
1749
  """Add a new Asset object to Salesforce.
1646
1750
 
1647
1751
  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)
1752
+ asset_name (str):
1753
+ The name of the Asset.
1754
+ product_id (str):
1755
+ Related Product ID.
1756
+ serial_number (str):
1757
+ Serial Number of the Asset.
1758
+ status (str):
1759
+ The status of the Asset.
1760
+ Typical values are "Purchased", "Shipped", "Installed", "Registered", "Obsolete"
1761
+ purchase_date (str):
1762
+ Purchase date of the Asset.
1763
+ install_date (str):
1764
+ Install date of the Asset.
1765
+ description (str):
1766
+ Description of the Asset.
1767
+ kwargs (dict):
1768
+ Additional values (e.g. custom fields)
1656
1769
 
1657
1770
  Returns:
1658
- dict | None: Dictionary with the Salesforce Asset data or None if the request fails.
1771
+ dict | None:
1772
+ Dictionary with the Salesforce Asset data or None if the request fails.
1773
+
1659
1774
  """
1660
1775
 
1661
1776
  if not self._access_token or not self._instance_url:
@@ -1676,33 +1791,20 @@ class Salesforce(object):
1676
1791
  payload["Description"] = description
1677
1792
  payload.update(kwargs) # Add additional fields from kwargs
1678
1793
 
1679
- logger.debug(
1680
- "Add Salesforce asset -> %s; calling -> %s", asset_name, request_url
1794
+ self.logger.debug(
1795
+ "Add Salesforce asset -> '%s'; calling -> %s",
1796
+ asset_name,
1797
+ request_url,
1681
1798
  )
1682
1799
 
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
1800
+ return self.do_request(
1801
+ method="POST",
1802
+ url=request_url,
1803
+ headers=request_header,
1804
+ data=json.dumps(payload),
1805
+ timeout=REQUEST_TIMEOUT,
1806
+ failure_message="Failed to add Salesforce asset -> '{}'".format(asset_name),
1807
+ )
1706
1808
 
1707
1809
  # end method definition
1708
1810
 
@@ -1712,23 +1814,43 @@ class Salesforce(object):
1712
1814
  start_date: str,
1713
1815
  contract_term: int,
1714
1816
  status: str = "Draft",
1715
- description: Optional[str] = None,
1716
- contract_type: Optional[str] = None,
1717
- **kwargs: Any,
1817
+ description: str | None = None,
1818
+ contract_type: str | None = None,
1819
+ **kwargs: dict[str, str],
1718
1820
  ) -> dict | None:
1719
1821
  """Add a new Contract object to Salesforce.
1720
1822
 
1721
1823
  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"
1824
+ account_id (str):
1825
+ The technical ID of the related Salesforce Account object.
1826
+ start_date (str):
1827
+ Start date of the contract. Use YYYY-MM-DD notation.
1828
+ contract_term (int):
1829
+ Term of the contract in number of months, e.g. 48 for 4 years term.
1830
+ The end date of the contract will be calculated from start date + term.
1831
+ contract_type (str):
1832
+ Type of the Contract. Typical values are:
1833
+ - "Subscription"
1834
+ - "Maintenance"
1835
+ - "Support"
1836
+ - "Lease"
1837
+ - "Service"
1838
+ status (str):
1839
+ Status of the Contract. Typical values are:
1840
+ - "Draft"
1841
+ - "Activated"
1842
+ - "In Approval Process"
1843
+ description (str, optional):
1844
+ Description of the contract.
1845
+ contract_type:
1846
+ Type name of the contract.
1847
+ kwargs:
1848
+ Additional keyword arguments.
1729
1849
 
1730
1850
  Returns:
1731
- dict | None: Dictionary with the Salesforce user data or None if the request fails.
1851
+ dict | None:
1852
+ Dictionary with the Salesforce contract data or None if the request fails.
1853
+
1732
1854
  """
1733
1855
 
1734
1856
  if not self._access_token or not self._instance_url:
@@ -1749,34 +1871,21 @@ class Salesforce(object):
1749
1871
  payload["ContractType"] = contract_type
1750
1872
  payload.update(kwargs) # Add additional fields from kwargs
1751
1873
 
1752
- logger.debug(
1753
- "Adding Salesforce contract for account ID -> %s; calling -> %s",
1874
+ self.logger.debug(
1875
+ "Adding Salesforce contract for account with ID -> %s; calling -> %s",
1754
1876
  account_id,
1755
1877
  request_url,
1756
1878
  )
1757
1879
 
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
1880
+ return self.do_request(
1881
+ method="POST",
1882
+ url=request_url,
1883
+ headers=request_header,
1884
+ data=json.dumps(payload),
1885
+ timeout=REQUEST_TIMEOUT,
1886
+ failure_message="Failed to add Salesforce contract for account with ID -> {}".format(
1887
+ account_id,
1888
+ ),
1889
+ )
1781
1890
 
1782
1891
  # end method definition