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

@@ -1,5 +1,6 @@
1
1
  """
2
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
3
4
 
4
5
  Class: Salesforce
5
6
  Methods:
@@ -7,22 +8,38 @@ Methods:
7
8
  __init__ : class initializer
8
9
  config : Returns config data set
9
10
  credentials: Returns the token data
11
+
10
12
  request_header: Returns the request header for Salesforce API calls
13
+ do_request: Call an Salesforce REST API in a safe way
11
14
  parse_request_response: Parse the REST API responses and convert
12
15
  them to Python dict in a safe way
13
16
  exist_result_item: Check if an dict item is in the response
14
17
  of the Salesforce API call
15
18
  get_result_value: Check if a defined value (based on a key) is in the Salesforce API response
16
19
 
17
- authenticate : Authenticates at Salesforce API
18
-
19
- get_user: Get a Salesforce user based on its ID.
20
- add_user: Add a new Salesforce user.
20
+ authenticate: Authenticates at Salesforce API
21
21
 
22
+ get_object_id_by_name: Get the ID of a given Salesforce object with a given type and name
22
23
  get_object: Get a Salesforce object based on a defined
23
24
  field value and return selected result fields.
24
25
  add_object: Add object to Salesforce. This is a generic wrapper method
25
26
  for the actual add methods.
27
+
28
+ get_group: Get a Salesforce group based on its ID.
29
+ add_group: Add a new Salesforce group.
30
+ update_group: Update a Salesforce group.
31
+ get_group_members: Get Salesforce group members
32
+ add_group_member: Add a user or group to a Salesforce group
33
+
34
+ get_all_user_profiles: Get all user profiles
35
+ get_user_profile_id: Get a user profile ID by profile name
36
+ get_user_id: Get a user ID by user name
37
+ get_user: Get a Salesforce user based on its ID.
38
+ add_user: Add a new Salesforce user.
39
+ update_user: Update a Salesforce user.
40
+ update_user_password: Update the password of a Salesforce user.
41
+ update_user_photo: update the Salesforce user photo.
42
+
26
43
  add_account: Add a new Account object to Salesforce.
27
44
  add_product: Add a new Product object to Salesforce.
28
45
  add_opportunity: Add a new Opportunity object to Salesfoce.
@@ -38,20 +55,28 @@ __credits__ = ["Kai-Philip Gatzweiler"]
38
55
  __maintainer__ = "Dr. Marc Diefenbruch"
39
56
  __email__ = "mdiefenb@opentext.com"
40
57
 
58
+ import os
41
59
  import json
42
60
  import logging
61
+ import time
43
62
 
44
63
  from typing import Optional, Union, Any
64
+
65
+ from http import HTTPStatus
45
66
  import requests
46
67
 
47
68
  logger = logging.getLogger("pyxecm.customizer.salesforce")
48
69
 
49
- request_login_headers = {
70
+ REQUEST_LOGIN_HEADERS = {
50
71
  "Content-Type": "application/x-www-form-urlencoded",
51
72
  "Accept": "application/json",
52
73
  }
53
74
 
54
75
  REQUEST_TIMEOUT = 60
76
+ REQUEST_RETRY_DELAY = 20
77
+ REQUEST_MAX_RETRIES = 3
78
+
79
+ SALESFORCE_API_VERSION = "v60.0"
55
80
 
56
81
  class Salesforce(object):
57
82
  """Used to retrieve and automate stettings in Salesforce."""
@@ -84,21 +109,56 @@ class Salesforce(object):
84
109
  security_token (str, optional): security token for Salesforce login
85
110
  """
86
111
 
112
+ # The instance URL is also returned by the authenticate call
113
+ # but typically it is identical to the base_url.
114
+ self._instance_url = base_url
115
+
87
116
  salesforce_config = {}
88
117
 
89
- # Set the authentication endpoints and credentials
90
- salesforce_config["baseUrl"] = base_url
118
+ # Store the credentials and parameters in a config dictionary:
91
119
  salesforce_config["clientId"] = client_id
92
120
  salesforce_config["clientSecret"] = client_secret
93
121
  salesforce_config["username"] = username
94
122
  salesforce_config["password"] = password
95
123
  salesforce_config["securityToken"] = security_token
124
+
125
+ # Set the Salesforce URLs and REST API endpoints:
126
+ salesforce_config["baseUrl"] = base_url
127
+ salesforce_config["objectUrl"] = salesforce_config[
128
+ "baseUrl"
129
+ ] + "/services/data/{}/sobjects/".format(SALESFORCE_API_VERSION)
130
+ salesforce_config["queryUrl"] = salesforce_config[
131
+ "baseUrl"
132
+ ] + "/services/data/{}/query/".format(SALESFORCE_API_VERSION)
133
+ salesforce_config["compositeUrl"] = salesforce_config[
134
+ "baseUrl"
135
+ ] + "/services/data/{}/composite/".format(SALESFORCE_API_VERSION)
136
+ salesforce_config["connectUrl"] = salesforce_config[
137
+ "baseUrl"
138
+ ] + "/services/data/{}/connect/".format(SALESFORCE_API_VERSION)
139
+ salesforce_config["toolingUrl"] = salesforce_config[
140
+ "baseUrl"
141
+ ] + "/services/data/{}/tooling/".format(SALESFORCE_API_VERSION)
96
142
  if authorization_url:
97
143
  salesforce_config["authenticationUrl"] = authorization_url
98
144
  else:
99
145
  salesforce_config["authenticationUrl"] = (
100
146
  salesforce_config["baseUrl"] + "/services/oauth2/token"
101
147
  )
148
+ # URLs that are based on the objectURL (sobjects/):
149
+ salesforce_config["userUrl"] = salesforce_config["objectUrl"] + "User/"
150
+ salesforce_config["groupUrl"] = salesforce_config["objectUrl"] + "Group/"
151
+ salesforce_config["groupMemberUrl"] = (
152
+ salesforce_config["objectUrl"] + "GroupMember/"
153
+ )
154
+ salesforce_config["accountUrl"] = salesforce_config["objectUrl"] + "Account/"
155
+ salesforce_config["productUrl"] = salesforce_config["objectUrl"] + "Product2/"
156
+ salesforce_config["opportunityUrl"] = (
157
+ salesforce_config["objectUrl"] + "Opportunity/"
158
+ )
159
+ salesforce_config["caseUrl"] = salesforce_config["objectUrl"] + "Case/"
160
+ salesforce_config["assetUrl"] = salesforce_config["objectUrl"] + "Asset/"
161
+ salesforce_config["contractUrl"] = salesforce_config["objectUrl"] + "Contract/"
102
162
 
103
163
  # Set the data for the token request
104
164
  salesforce_config["authenticationData"] = {
@@ -111,6 +171,8 @@ class Salesforce(object):
111
171
 
112
172
  self._config = salesforce_config
113
173
 
174
+ # end method definition
175
+
114
176
  def config(self) -> dict:
115
177
  """Returns the configuration dictionary
116
178
 
@@ -143,12 +205,187 @@ class Salesforce(object):
143
205
 
144
206
  request_header = {
145
207
  "Authorization": "Bearer {}".format(self._access_token),
146
- "Content-Type": content_type,
147
208
  }
209
+ if content_type:
210
+ request_header["Content-Type"] = content_type
211
+
148
212
  return request_header
149
213
 
150
214
  # end method definition
151
215
 
216
+ def do_request(
217
+ self,
218
+ url: str,
219
+ method: str = "GET",
220
+ headers: dict | None = None,
221
+ data: dict | None = None,
222
+ json_data: dict | None = None,
223
+ files: dict | None = None,
224
+ params: dict | None = None,
225
+ timeout: int | None = REQUEST_TIMEOUT,
226
+ show_error: bool = True,
227
+ show_warning: bool = False,
228
+ warning_message: str = "",
229
+ failure_message: str = "",
230
+ success_message: str = "",
231
+ max_retries: int = REQUEST_MAX_RETRIES,
232
+ retry_forever: bool = False,
233
+ parse_request_response: bool = True,
234
+ stream: bool = False,
235
+ verify: bool = True,
236
+ ) -> dict | None:
237
+ """Call an Salesforce REST API in a safe way
238
+
239
+ Args:
240
+ url (str): URL to send the request to.
241
+ method (str, optional): HTTP method (GET, POST, etc.). Defaults to "GET".
242
+ headers (dict | None, optional): Request Headers. Defaults to None.
243
+ data (dict | None, optional): Request payload. Defaults to None
244
+ files (dict | None, optional): Dictionary of {"name": file-tuple} for multipart encoding upload.
245
+ file-tuple can be a 2-tuple ("filename", fileobj) or a 3-tuple ("filename", fileobj, "content_type")
246
+ params (dict | None, optional): Add key-value pairs to the query string of the URL.
247
+ When you use the params parameter, requests automatically appends
248
+ the key-value pairs to the URL as part of the query string
249
+ timeout (int | None, optional): Timeout for the request in seconds. Defaults to REQUEST_TIMEOUT.
250
+ show_error (bool, optional): Whether or not an error should be logged in case of a failed REST call.
251
+ If False, then only a warning is logged. Defaults to True.
252
+ warning_message (str, optional): Specific warning message. Defaults to "". If not given the error_message will be used.
253
+ failure_message (str, optional): Specific error message. Defaults to "".
254
+ success_message (str, optional): Specific success message. Defaults to "".
255
+ max_retries (int, optional): How many retries on Connection errors? Default is REQUEST_MAX_RETRIES.
256
+ retry_forever (bool, optional): Eventually wait forever - without timeout. Defaults to False.
257
+ parse_request_response (bool, optional): should the response.text be interpreted as json and loaded into a dictionary. True is the default.
258
+ stream (bool, optional): parameter is used to control whether the response content should be immediately downloaded or streamed incrementally
259
+ verify (bool, optional): specify whether or not SSL certificates should be verified when making an HTTPS request. Default = True
260
+
261
+ Returns:
262
+ dict | None: Response of OTDS REST API or None in case of an error.
263
+ """
264
+
265
+ if headers is None:
266
+ logger.error("Missing request header. Cannot send request to Core Share!")
267
+ return None
268
+
269
+ # In case of an expired session we reauthenticate and
270
+ # try 1 more time. Session expiration should not happen
271
+ # twice in a row:
272
+ retries = 0
273
+
274
+ while True:
275
+ try:
276
+ response = requests.request(
277
+ method=method,
278
+ url=url,
279
+ data=data,
280
+ json=json_data,
281
+ files=files,
282
+ params=params,
283
+ headers=headers,
284
+ timeout=timeout,
285
+ stream=stream,
286
+ verify=verify,
287
+ )
288
+
289
+ if response.ok:
290
+ if success_message:
291
+ logger.info(success_message)
292
+ if parse_request_response:
293
+ return self.parse_request_response(response)
294
+ else:
295
+ return response
296
+ # Check if Session has expired - then re-authenticate and try once more
297
+ elif response.status_code == 401 and retries == 0:
298
+ logger.debug("Session has expired - try to re-authenticate...")
299
+ self.authenticate(revalidate=True)
300
+ # Make sure to not change an existing content type
301
+ # the do_request() method is called with:
302
+ headers = self.request_header(
303
+ content_type=headers.get("Content-Type", None)
304
+ )
305
+ retries += 1
306
+ else:
307
+ # Handle plain HTML responses to not pollute the logs
308
+ content_type = response.headers.get("content-type", None)
309
+ if content_type == "text/html":
310
+ response_text = "HTML content (only printed in debug log)"
311
+ else:
312
+ response_text = response.text
313
+
314
+ if show_error:
315
+ logger.error(
316
+ "%s; status -> %s/%s; error -> %s",
317
+ failure_message,
318
+ response.status_code,
319
+ HTTPStatus(response.status_code).phrase,
320
+ response_text,
321
+ )
322
+ elif show_warning:
323
+ logger.warning(
324
+ "%s; status -> %s/%s; warning -> %s",
325
+ warning_message if warning_message else failure_message,
326
+ response.status_code,
327
+ HTTPStatus(response.status_code).phrase,
328
+ response_text,
329
+ )
330
+ if content_type == "text/html":
331
+ logger.debug(
332
+ "%s; status -> %s/%s; warning -> %s",
333
+ failure_message,
334
+ response.status_code,
335
+ HTTPStatus(response.status_code).phrase,
336
+ response.text,
337
+ )
338
+ return None
339
+ except requests.exceptions.Timeout:
340
+ if retries <= max_retries:
341
+ logger.warning(
342
+ "Request timed out. Retrying in %s seconds...",
343
+ str(REQUEST_RETRY_DELAY),
344
+ )
345
+ retries += 1
346
+ time.sleep(REQUEST_RETRY_DELAY) # Add a delay before retrying
347
+ else:
348
+ logger.error(
349
+ "%s; timeout error",
350
+ failure_message,
351
+ )
352
+ if retry_forever:
353
+ # If it fails after REQUEST_MAX_RETRIES retries we let it wait forever
354
+ logger.warning("Turn timeouts off and wait forever...")
355
+ timeout = None
356
+ else:
357
+ return None
358
+ except requests.exceptions.ConnectionError:
359
+ if retries <= max_retries:
360
+ logger.warning(
361
+ "Connection error. Retrying in %s seconds...",
362
+ str(REQUEST_RETRY_DELAY),
363
+ )
364
+ retries += 1
365
+ time.sleep(REQUEST_RETRY_DELAY) # Add a delay before retrying
366
+ else:
367
+ logger.error(
368
+ "%s; connection error",
369
+ failure_message,
370
+ )
371
+ if retry_forever:
372
+ # If it fails after REQUEST_MAX_RETRIES retries we let it wait forever
373
+ logger.warning("Turn timeouts off and wait forever...")
374
+ timeout = None
375
+ time.sleep(REQUEST_RETRY_DELAY) # Add a delay before retrying
376
+ else:
377
+ return None
378
+ # end try
379
+ logger.debug(
380
+ "Retrying REST API %s call -> %s... (retry = %s)",
381
+ method,
382
+ url,
383
+ str(retries),
384
+ )
385
+ # end while True
386
+
387
+ # end method definition
388
+
152
389
  def parse_request_response(
153
390
  self,
154
391
  response_object: requests.Response,
@@ -278,16 +515,16 @@ class Salesforce(object):
278
515
 
279
516
  # Already authenticated and session still valid?
280
517
  if self._access_token and not revalidate:
281
- logger.info(
518
+ logger.debug(
282
519
  "Session still valid - return existing access token -> %s",
283
520
  str(self._access_token),
284
521
  )
285
522
  return self._access_token
286
523
 
287
524
  request_url = self.config()["authenticationUrl"]
288
- request_header = request_login_headers
525
+ request_header = REQUEST_LOGIN_HEADERS
289
526
 
290
- logger.info("Requesting Salesforce Access Token from -> %s", request_url)
527
+ logger.debug("Requesting Salesforce Access Token from -> %s", request_url)
291
528
 
292
529
  authenticate_post_body = self.credentials()
293
530
 
@@ -346,70 +583,27 @@ class Salesforce(object):
346
583
  """
347
584
 
348
585
  if not self._access_token or not self._instance_url:
349
- logger.error("Authentication required.")
350
- return None
586
+ self.authenticate()
351
587
 
352
588
  request_header = self.request_header()
353
- request_url = f"{self._instance_url}/services/data/v52.0/query/"
589
+ request_url = self.config()["queryUrl"]
354
590
 
355
591
  query = f"SELECT Id FROM {object_type} WHERE {name_field} = '{name}'"
356
592
 
357
- retries = 0
358
- while True:
359
- response = requests.get(
360
- request_url,
361
- headers=request_header,
362
- params={"q": query},
363
- timeout=REQUEST_TIMEOUT,
364
- )
365
- if response.ok:
366
- response = self.parse_request_response(response)
367
- object_id = self.get_result_value(response, "Id")
368
- return object_id
369
- elif response.status_code == 401 and retries == 0:
370
- logger.warning("Session has expired - try to re-authenticate...")
371
- self.authenticate(revalidate=True)
372
- request_header = self.request_header()
373
- retries += 1
374
- else:
375
- logger.error(
376
- "Failed to get Salesforce object ID for object type -> %s and object name -> %s; status -> %s; error -> %s",
377
- object_type,
378
- name,
379
- response.status_code,
380
- response.text,
381
- )
382
- return None
383
-
384
- # end method definition
385
-
386
- def get_profile_id(self, profile_name: str) -> Optional[str]:
387
- """Get a user profile ID by profile name.
388
-
389
- Args:
390
- profile_name (str): Name of the User Profile.
391
-
392
- Returns:
393
- Optional[str]: Technical ID of the user profile.
394
- """
395
-
396
- return self.get_object_id_by_name(object_type="Profile", name=profile_name)
397
-
398
- # end method definition
399
-
400
- def get_user_id(self, username: str) -> Optional[str]:
401
- """Get a user ID by user name.
402
-
403
- Args:
404
- username (str): Name of the User.
405
-
406
- Returns:
407
- Optional[str]: Technical ID of the user
408
- """
409
-
410
- return self.get_object_id_by_name(
411
- object_type="User", name=username, name_field="Username"
593
+ response = self.do_request(
594
+ method="GET",
595
+ url=request_url,
596
+ headers=request_header,
597
+ params={"q": query},
598
+ timeout=REQUEST_TIMEOUT,
599
+ failure_message="Failed to get Salesforce object ID for object type -> '{}' and object name -> '{}'".format(
600
+ object_type, name
601
+ ),
412
602
  )
603
+ if not response:
604
+ return None
605
+
606
+ return self.get_result_value(response, "Id")
413
607
 
414
608
  # end method definition
415
609
 
@@ -433,11 +627,33 @@ class Salesforce(object):
433
627
 
434
628
  Returns:
435
629
  dict | None: Dictionary with the Salesforce object data.
630
+
631
+ Example response:
632
+ {
633
+ 'totalSize': 2,
634
+ 'done': True,
635
+ 'records': [
636
+ {
637
+ 'attributes': {
638
+ 'type': 'Opportunity',
639
+ 'url': '/services/data/v60.0/sobjects/Opportunity/006Dn00000EclybIAB'
640
+ },
641
+ 'Id': '006Dn00000EclybIAB'
642
+ },
643
+ {
644
+ 'attributes': {
645
+ 'type': 'Opportunity',
646
+ 'url': '/services/data/v60.0/sobjects/Opportunity/006Dn00000EclyfIAB'
647
+ },
648
+ 'Id': '006Dn00000EclyfIAB'
649
+ }
650
+ ]
651
+ }
436
652
  """
437
653
 
438
654
  if not self._access_token or not self._instance_url:
439
- logger.error("Authentication required.")
440
- return None
655
+ self.authenticate()
656
+
441
657
  if search_field and not search_value:
442
658
  logger.error(
443
659
  "No search value has been provided for search field -> %s!",
@@ -445,7 +661,7 @@ class Salesforce(object):
445
661
  )
446
662
  return None
447
663
  if not result_fields:
448
- logger.info(
664
+ logger.debug(
449
665
  "No result fields defined. Using 'FIELDS(STANDARD)' to deliver all standard fields of the object."
450
666
  )
451
667
  result_fields = ["FIELDS(STANDARD)"]
@@ -456,32 +672,21 @@ class Salesforce(object):
456
672
  query += " LIMIT {}".format(str(limit))
457
673
 
458
674
  request_header = self.request_header()
459
- request_url = f"{self._instance_url}/services/data/v52.0/query/?q={query}"
675
+ request_url = self.config()["queryUrl"] + "?q={}".format(query)
460
676
 
461
- logger.info(
677
+ logger.debug(
462
678
  "Sending query -> %s to Salesforce; calling -> %s", query, request_url
463
679
  )
464
680
 
465
- retries = 0
466
- while True:
467
- response = requests.get(request_url, headers=request_header, timeout=30)
468
- if response.ok:
469
- return self.parse_request_response(response)
470
- elif response.status_code == 401 and retries == 0:
471
- logger.warning("Session has expired - try to re-authenticate...")
472
- self.authenticate(revalidate=True)
473
- request_header = self.request_header()
474
- retries += 1
475
- else:
476
- logger.error(
477
- "Failed to retrieve Salesforce object -> %s with %s = %s; status -> %s; error -> %s",
478
- object_type,
479
- search_field,
480
- search_value,
481
- response.status_code,
482
- response.text,
483
- )
484
- return None
681
+ return self.do_request(
682
+ method="GET",
683
+ url=request_url,
684
+ headers=request_header,
685
+ timeout=REQUEST_TIMEOUT,
686
+ failure_message="Failed to retrieve Salesforce object type -> '{}' with {} = {}".format(
687
+ object_type, search_field, search_value
688
+ ),
689
+ )
485
690
 
486
691
  # end method definition
487
692
 
@@ -491,9 +696,10 @@ class Salesforce(object):
491
696
 
492
697
  Args:
493
698
  object_type (str): Type of the Salesforce business object, like "Account" or "Case".
699
+ **kwargs (dict): keyword / value ictionary with additional parameters
494
700
 
495
701
  Returns:
496
- dict | None: Dictionary with the Salesforce Case data or None if the request fails.
702
+ dict | None: Dictionary with the Salesforce object data or None if the request fails.
497
703
  """
498
704
 
499
705
  match object_type:
@@ -568,49 +774,237 @@ class Salesforce(object):
568
774
 
569
775
  # end method definition
570
776
 
571
- def get_user(self, user_id: str) -> dict | None:
572
- """Get a Salesforce user based on its ID.
777
+ def get_group_id(self, groupname: str) -> Optional[str]:
778
+ """Get a group ID by group name.
573
779
 
574
780
  Args:
575
- user_id (str): ID of the Salesforce user
781
+ groupname (str): Name of the Group.
576
782
 
577
783
  Returns:
578
- dict | None: Dictionary with the Salesforce user data or None if the request fails.
784
+ Optional[str]: Technical ID of the group
785
+ """
786
+
787
+ return self.get_object_id_by_name(
788
+ object_type="Group", name=groupname, name_field="Name"
789
+ )
790
+
791
+ # end method definition
792
+
793
+ def get_group(self, group_id: str) -> dict | None:
794
+ """Get a Salesforce group based on its ID.
795
+
796
+ Args:
797
+ group_id (str): ID of the Salesforce group
798
+
799
+ Returns:
800
+ dict | None: Dictionary with the Salesforce group data or None if the request fails.
579
801
  """
580
802
 
581
803
  if not self._access_token or not self._instance_url:
582
- logger.error("Authentication required.")
583
- return None
804
+ self.authenticate()
584
805
 
585
806
  request_header = self.request_header()
586
- request_url = (
587
- f"{self._instance_url}/services/data/v52.0/sobjects/User/{user_id}"
807
+ request_url = self.config()["groupUrl"] + group_id
808
+
809
+ logger.debug(
810
+ "Get Salesforce group with ID -> %s; calling -> %s", group_id, request_url
588
811
  )
589
812
 
590
- logger.info(
591
- "Get Salesforce user with ID -> %s; calling -> %s", user_id, request_url
813
+ return self.do_request(
814
+ method="GET",
815
+ url=request_url,
816
+ headers=request_header,
817
+ timeout=REQUEST_TIMEOUT,
818
+ failure_message="Failed to get Salesforce group with ID -> {}".format(
819
+ group_id
820
+ ),
592
821
  )
593
822
 
594
- retries = 0
595
- while True:
596
- response = requests.get(
597
- request_url, headers=request_header, timeout=REQUEST_TIMEOUT
598
- )
599
- if response.ok:
600
- return self.parse_request_response(response)
601
- elif response.status_code == 401 and retries == 0:
602
- logger.warning("Session has expired - try to re-authenticate...")
603
- self.authenticate(revalidate=True)
604
- request_header = self.request_header()
605
- retries += 1
606
- else:
607
- logger.error(
608
- "Failed to get Salesforce user -> %s; status -> %s; error -> %s",
609
- user_id,
610
- response.status_code,
611
- response.text,
612
- )
613
- return None
823
+ # end method definition
824
+
825
+ def add_group(
826
+ self,
827
+ group_name: str,
828
+ group_type: str = "Regular",
829
+ ) -> dict | None:
830
+ """Add a new Salesforce group.
831
+
832
+ Args:
833
+ group_name (str): Name of the new Salesforce group
834
+
835
+ Returns:
836
+ dict | None: Dictionary with the Salesforce Group data or None if the request fails.
837
+
838
+ Example response:
839
+ {
840
+ 'id': '00GDn000000KWE0MAO',
841
+ 'success': True,
842
+ 'errors': []
843
+ }
844
+ """
845
+
846
+ if not self._access_token or not self._instance_url:
847
+ self.authenticate()
848
+
849
+ request_header = self.request_header()
850
+ request_url = self.config()["groupUrl"]
851
+
852
+ payload = {"Name": group_name, "Type": group_type}
853
+
854
+ logger.debug(
855
+ "Adding Salesforce group -> %s; calling -> %s", group_name, request_url
856
+ )
857
+
858
+ return self.do_request(
859
+ method="POST",
860
+ url=request_url,
861
+ headers=request_header,
862
+ data=json.dumps(payload),
863
+ timeout=REQUEST_TIMEOUT,
864
+ failure_message="Failed to add Salesforce group -> '{}'".format(group_name),
865
+ )
866
+
867
+ # end method definition
868
+
869
+ def update_group(
870
+ self,
871
+ group_id: str,
872
+ update_data: dict,
873
+ ) -> dict | None:
874
+ """Update a Salesforce group.
875
+
876
+ Args:
877
+ group_id (str): The Salesforce group ID.
878
+ update_data (dict): Dictionary containing the fields to update.
879
+
880
+ Returns:
881
+ dict: Response from the Salesforce API.
882
+ """
883
+
884
+ if not self._access_token or not self._instance_url:
885
+ self.authenticate()
886
+
887
+ request_header = self.request_header()
888
+
889
+ request_url = self.config()["groupUrl"] + group_id
890
+
891
+ logger.debug(
892
+ "Update Salesforce group with ID -> %s; calling -> %s",
893
+ group_id,
894
+ request_url,
895
+ )
896
+
897
+ return self.do_request(
898
+ method="PATCH",
899
+ url=request_url,
900
+ headers=request_header,
901
+ json_data=update_data,
902
+ timeout=REQUEST_TIMEOUT,
903
+ failure_message="Failed to update Salesforce group with ID -> {}".format(
904
+ group_id
905
+ ),
906
+ )
907
+
908
+ # end method definition
909
+
910
+ def get_group_members(self, group_id: str) -> list | None:
911
+ """Get Salesforce group members
912
+
913
+ Args:
914
+ group_id (str): Id of the group to retrieve the members
915
+
916
+ Returns:
917
+ list | None: result
918
+
919
+ Example response:
920
+ {
921
+ 'totalSize': 1,
922
+ 'done': True,
923
+ 'records': [
924
+ {
925
+ 'attributes': {
926
+ 'type': 'GroupMember',
927
+ 'url': '/services/data/v60.0/sobjects/GroupMember/011Dn000000ELhwIAG'
928
+ },
929
+ 'UserOrGroupId': '00GDn000000KWE5MAO'
930
+ }
931
+ ]
932
+ }
933
+ """
934
+
935
+ if not self._access_token or not self._instance_url:
936
+ self.authenticate()
937
+
938
+ request_header = self.request_header()
939
+
940
+ request_url = self.config()["queryUrl"]
941
+
942
+ query = f"SELECT UserOrGroupId FROM GroupMember WHERE GroupId = '{group_id}'"
943
+ params = {"q": query}
944
+
945
+ logger.debug(
946
+ "Get members of Salesforce group with ID -> %s; calling -> %s",
947
+ group_id,
948
+ request_url,
949
+ )
950
+
951
+ return self.do_request(
952
+ method="GET",
953
+ url=request_url,
954
+ headers=request_header,
955
+ params=params,
956
+ timeout=REQUEST_TIMEOUT,
957
+ failure_message="Failed to get members of Salesforce group with ID -> {}".format(
958
+ group_id
959
+ ),
960
+ )
961
+
962
+ # end method definition
963
+
964
+ def add_group_member(self, group_id: str, member_id: str) -> dict | None:
965
+ """Add a user or group to a Salesforce group
966
+
967
+ Args:
968
+ group_id (str): ID of the Salesforce Group to add member to.
969
+ member_id (str): ID of the user or group.
970
+
971
+ Returns:
972
+ dict | None: Dictionary with the Salesforce membership data or None if the request fails.
973
+
974
+ Example response (id is the membership ID):
975
+ {
976
+ 'id': '011Dn000000ELhwIAG',
977
+ 'success': True,
978
+ 'errors': []
979
+ }
980
+ """
981
+
982
+ if not self._access_token or not self._instance_url:
983
+ self.authenticate()
984
+
985
+ request_url = self.config()["groupMemberUrl"]
986
+
987
+ request_header = self.request_header()
988
+
989
+ payload = {"GroupId": group_id, "UserOrGroupId": member_id}
990
+
991
+ logger.debug(
992
+ "Add member with ID -> %s to Salesforce group with ID -> %s; calling -> %s",
993
+ member_id,
994
+ group_id,
995
+ request_url,
996
+ )
997
+
998
+ return self.do_request(
999
+ method="POST",
1000
+ url=request_url,
1001
+ headers=request_header,
1002
+ json_data=payload,
1003
+ timeout=REQUEST_TIMEOUT,
1004
+ failure_message="Failed to add member with ID -> {} to Salesforce group with ID -> {}".format(
1005
+ member_id, group_id
1006
+ ),
1007
+ )
614
1008
 
615
1009
  # end method definition
616
1010
 
@@ -632,8 +1026,7 @@ class Salesforce(object):
632
1026
  'url': '/services/data/v52.0/sobjects/Profile/00eDn000001msL8IAI'},
633
1027
  'Id': '00eDn000001msL8IAI',
634
1028
  'Name': 'Standard User',
635
- 'CreatedById':
636
- '005Dn000001rRodIAE',
1029
+ 'CreatedById': '005Dn000001rRodIAE',
637
1030
  'CreatedDate': '2022-11-30T15:30:54.000+0000',
638
1031
  'Description': None,
639
1032
  'LastModifiedById': '005Dn000001rUacIAE',
@@ -648,36 +1041,83 @@ class Salesforce(object):
648
1041
  """
649
1042
 
650
1043
  if not self._access_token or not self._instance_url:
651
- logger.error("Authentication required.")
652
- return None
1044
+ self.authenticate()
653
1045
 
654
1046
  request_header = self.request_header()
655
- request_url = f"{self._instance_url}/services/data/v52.0/query/"
1047
+ request_url = self.config()["queryUrl"]
656
1048
 
657
1049
  query = "SELECT Id, Name, CreatedById, CreatedDate, Description, LastModifiedById, LastModifiedDate, PermissionsCustomizeApplication, PermissionsEditTask, PermissionsImportLeads FROM Profile"
658
1050
 
659
- retries = 0
660
- while True:
661
- response = requests.get(
662
- request_url,
663
- headers=request_header,
664
- params={"q": query},
665
- timeout=REQUEST_TIMEOUT,
666
- )
667
- if response.ok:
668
- return self.parse_request_response(response)
669
- elif response.status_code == 401 and retries == 0:
670
- logger.warning("Session has expired - try to re-authenticate...")
671
- self.authenticate(revalidate=True)
672
- request_header = self.request_header()
673
- retries += 1
674
- else:
675
- logger.error(
676
- "Failed to get Salesforce user profiles; status -> %s; error -> %s",
677
- response.status_code,
678
- response.text,
679
- )
680
- return None
1051
+ return self.do_request(
1052
+ method="GET",
1053
+ url=request_url,
1054
+ headers=request_header,
1055
+ params={"q": query},
1056
+ timeout=REQUEST_TIMEOUT,
1057
+ failure_message="Failed to get Salesforce user profiles",
1058
+ )
1059
+
1060
+ # end method definition
1061
+
1062
+ def get_user_profile_id(self, profile_name: str) -> Optional[str]:
1063
+ """Get a user profile ID by profile name.
1064
+
1065
+ Args:
1066
+ profile_name (str): Name of the User Profile.
1067
+
1068
+ Returns:
1069
+ Optional[str]: Technical ID of the user profile.
1070
+ """
1071
+
1072
+ return self.get_object_id_by_name(object_type="Profile", name=profile_name)
1073
+
1074
+ # end method definition
1075
+
1076
+ def get_user_id(self, username: str) -> Optional[str]:
1077
+ """Get a user ID by user name.
1078
+
1079
+ Args:
1080
+ username (str): Name of the User.
1081
+
1082
+ Returns:
1083
+ Optional[str]: Technical ID of the user
1084
+ """
1085
+
1086
+ return self.get_object_id_by_name(
1087
+ object_type="User", name=username, name_field="Username"
1088
+ )
1089
+
1090
+ # end method definition
1091
+
1092
+ def get_user(self, user_id: str) -> dict | None:
1093
+ """Get a Salesforce user based on its ID.
1094
+
1095
+ Args:
1096
+ user_id (str): ID of the Salesforce user
1097
+
1098
+ Returns:
1099
+ dict | None: Dictionary with the Salesforce user data or None if the request fails.
1100
+ """
1101
+
1102
+ if not self._access_token or not self._instance_url:
1103
+ self.authenticate()
1104
+
1105
+ request_header = self.request_header()
1106
+ request_url = self.config()["userUrl"] + user_id
1107
+
1108
+ logger.debug(
1109
+ "Get Salesforce user with ID -> %s; calling -> %s", user_id, request_url
1110
+ )
1111
+
1112
+ return self.do_request(
1113
+ method="GET",
1114
+ url=request_url,
1115
+ headers=request_header,
1116
+ timeout=REQUEST_TIMEOUT,
1117
+ failure_message="Failed to get Salesforce user with ID -> {}".format(
1118
+ user_id
1119
+ ),
1120
+ )
681
1121
 
682
1122
  # end method definition
683
1123
 
@@ -685,23 +1125,35 @@ class Salesforce(object):
685
1125
  self,
686
1126
  username: str,
687
1127
  email: str,
688
- password: str,
689
1128
  firstname: str,
690
1129
  lastname: str,
1130
+ title: str | None = None,
1131
+ department: str | None = None,
1132
+ company_name: str = "Innovate",
1133
+ profile_name: Optional[str] = "Standard User",
691
1134
  profile_id: Optional[str] = None,
1135
+ time_zone_key: Optional[str] = "America/Los_Angeles",
1136
+ email_encoding_key: Optional[str] = "ISO-8859-1",
1137
+ locale_key: Optional[str] = "en_US",
692
1138
  alias: Optional[str] = None,
693
1139
  ) -> dict | None:
694
- """Add a new Salesforce user.
1140
+ """Add a new Salesforce user. The password has to be set separately.
695
1141
 
696
1142
  Args:
697
1143
  username (str): Login name of the new user
698
1144
  email (str): Email of the new user
699
- password (str): Password of the new user
700
1145
  firstname (str): First name of the new user.
701
1146
  lastname (str): Last name of the new user.
1147
+ title (str): Title of the user.
1148
+ department (str): Department of the user.
1149
+ company_name (str): Name of the Company of the user.
1150
+ profile_name (str): Profile name like "Standard User"
702
1151
  profile_id (str, optional): Profile ID of the new user. Defaults to None.
703
1152
  Use method get_all_user_profiles() to determine
704
- the desired Profile for the user.
1153
+ the desired Profile for the user. Or pass the profile_name.
1154
+ time_zone_key (str, optional) in format country/city like "America/Los_Angeles",
1155
+ email_encoding_key (str, optional). Default is "ISO-8859-1".
1156
+ locale_key (str, optional). Default is "en_US".
705
1157
  alias (str, optional): Alias of the new user. Defaults to None.
706
1158
 
707
1159
  Returns:
@@ -709,49 +1161,193 @@ class Salesforce(object):
709
1161
  """
710
1162
 
711
1163
  if not self._access_token or not self._instance_url:
712
- logger.error("Authentication required.")
713
- return None
1164
+ self.authenticate()
714
1165
 
715
1166
  request_header = self.request_header()
716
- request_url = f"{self._instance_url}/services/data/v52.0/sobjects/User/"
1167
+ request_url = self.config()["userUrl"]
1168
+
1169
+ # if just a profile name is given then we determine the profile ID by the name:
1170
+ if profile_name and not profile_id:
1171
+ profile_id = self.get_user_profile_id(profile_name)
717
1172
 
718
1173
  payload = {
719
1174
  "Username": username,
720
1175
  "Email": email,
721
- "Password": password,
722
1176
  "FirstName": firstname,
723
1177
  "LastName": lastname,
724
1178
  "ProfileId": profile_id,
725
- "Alias": alias,
1179
+ "Department": department,
1180
+ "CompanyName": company_name,
1181
+ "Title": title,
1182
+ "Alias": alias if alias else username,
1183
+ "TimeZoneSidKey": time_zone_key, # Set default TimeZoneSidKey
1184
+ "LocaleSidKey": locale_key, # Set default LocaleSidKey
1185
+ "EmailEncodingKey": email_encoding_key, # Set default EmailEncodingKey
1186
+ "LanguageLocaleKey": locale_key, # Set default LanguageLocaleKey
726
1187
  }
727
1188
 
728
- logger.info(
1189
+ logger.debug(
729
1190
  "Adding Salesforce user -> %s; calling -> %s", username, request_url
730
1191
  )
731
1192
 
732
- retries = 0
733
- while True:
734
- response = requests.post(
735
- request_url,
736
- headers=request_header,
737
- data=json.dumps(payload),
738
- timeout=REQUEST_TIMEOUT,
1193
+ return self.do_request(
1194
+ method="POST",
1195
+ url=request_url,
1196
+ headers=request_header,
1197
+ data=json.dumps(payload),
1198
+ timeout=REQUEST_TIMEOUT,
1199
+ failure_message="Failed to add Salesforce user -> {}".format(username),
1200
+ )
1201
+
1202
+ # end method definition
1203
+
1204
+ def update_user(
1205
+ self,
1206
+ user_id: str,
1207
+ update_data: dict,
1208
+ ) -> dict:
1209
+ """Update a Salesforce user.
1210
+
1211
+ Args:
1212
+ user_id (str): The Salesforce user ID.
1213
+ update_data (dict): Dictionary containing the fields to update.
1214
+
1215
+ Returns:
1216
+ dict: Response from the Salesforce API.
1217
+ """
1218
+
1219
+ if not self._access_token or not self._instance_url:
1220
+ self.authenticate()
1221
+
1222
+ request_header = self.request_header()
1223
+
1224
+ request_url = self.config()["userUrl"] + user_id
1225
+
1226
+ logger.debug(
1227
+ "Update Salesforce user with ID -> %s; calling -> %s", user_id, request_url
1228
+ )
1229
+
1230
+ return self.do_request(
1231
+ method="PATCH",
1232
+ url=request_url,
1233
+ headers=request_header,
1234
+ json_data=update_data,
1235
+ timeout=REQUEST_TIMEOUT,
1236
+ failure_message="Failed to update Salesforce user with ID -> {}".format(
1237
+ user_id
1238
+ ),
1239
+ )
1240
+
1241
+ # end method definition
1242
+
1243
+ def update_user_password(
1244
+ self,
1245
+ user_id: str,
1246
+ password: str,
1247
+ ) -> dict:
1248
+ """Update the password of a Salesforce user.
1249
+
1250
+ Args:
1251
+ user_id (str): The Salesforce user ID.
1252
+ password (str): New user password.
1253
+
1254
+ Returns:
1255
+ dict: Response from the Salesforce API.
1256
+ """
1257
+
1258
+ if not self._access_token or not self._instance_url:
1259
+ self.authenticate()
1260
+
1261
+ request_header = self.request_header()
1262
+
1263
+ request_url = self.config()["userUrl"] + "{}/password".format(user_id)
1264
+
1265
+ logger.debug(
1266
+ "Update password of Salesforce user with ID -> %s; calling -> %s",
1267
+ user_id,
1268
+ request_url,
1269
+ )
1270
+
1271
+ update_data = {"NewPassword": password}
1272
+
1273
+ return self.do_request(
1274
+ method="POST",
1275
+ url=request_url,
1276
+ headers=request_header,
1277
+ json_data=update_data,
1278
+ timeout=REQUEST_TIMEOUT,
1279
+ failure_message="Failed to update password of Salesforce user with ID -> {}".format(
1280
+ user_id
1281
+ ),
1282
+ )
1283
+
1284
+ # end method definition
1285
+
1286
+ def update_user_photo(
1287
+ self,
1288
+ user_id: str,
1289
+ photo_path: str,
1290
+ ) -> dict | None:
1291
+ """Update the Salesforce user photo.
1292
+
1293
+ Args:
1294
+ user_id (str): Salesforce ID of the user
1295
+ photo_path (str): file system path with the location of the photo
1296
+ Returns:
1297
+ dict | None: Dictionary with the Salesforce User data or None if the request fails.
1298
+ """
1299
+
1300
+ if not self._access_token or not self._instance_url:
1301
+ self.authenticate()
1302
+
1303
+ # Check if the photo file exists
1304
+ if not os.path.isfile(photo_path):
1305
+ logger.error("Photo file -> %s not found!", photo_path)
1306
+ return None
1307
+
1308
+ try:
1309
+ # Read the photo file as binary data
1310
+ with open(photo_path, "rb") as image_file:
1311
+ photo_data = image_file.read()
1312
+ except OSError as exception:
1313
+ # Handle any errors that occurred while reading the photo file
1314
+ logger.error(
1315
+ "Error reading photo file -> %s; error -> %s", photo_path, exception
739
1316
  )
740
- if response.ok:
741
- return self.parse_request_response(response)
742
- elif response.status_code == 401 and retries == 0:
743
- logger.warning("Session has expired - try to re-authenticate...")
744
- self.authenticate(revalidate=True)
745
- request_header = self.request_header()
746
- retries += 1
747
- else:
748
- logger.error(
749
- "Failed to add Salesforce user -> %s; status -> %s; error -> %s",
750
- username,
751
- response.status_code,
752
- response.text,
753
- )
754
- return None
1317
+ return None
1318
+
1319
+ # Content Type = None is important as upload calls need
1320
+ # a multipart header that is automatically selected if None is used:
1321
+ request_header = self.request_header(content_type=None)
1322
+
1323
+ data = {"json": json.dumps({"cropX": 0, "cropY": 0, "cropSize": 200})}
1324
+ request_url = self.config()["connectUrl"] + f"user-profiles/{user_id}/photo"
1325
+ files = {
1326
+ "fileUpload": (
1327
+ photo_path,
1328
+ photo_data,
1329
+ "application/octet-stream",
1330
+ )
1331
+ }
1332
+
1333
+ logger.debug(
1334
+ "Update profile photo of Salesforce user with ID -> %s; calling -> %s",
1335
+ user_id,
1336
+ request_url,
1337
+ )
1338
+
1339
+ return self.do_request(
1340
+ method="POST",
1341
+ url=request_url,
1342
+ headers=request_header,
1343
+ files=files,
1344
+ data=data,
1345
+ timeout=REQUEST_TIMEOUT,
1346
+ failure_message="Failed to update profile photo of Salesforce user with ID -> {}".format(
1347
+ user_id
1348
+ ),
1349
+ verify=False,
1350
+ )
755
1351
 
756
1352
  # end method definition
757
1353
 
@@ -783,11 +1379,10 @@ class Salesforce(object):
783
1379
  """
784
1380
 
785
1381
  if not self._access_token or not self._instance_url:
786
- logger.error("Authentication required.")
787
- return None
1382
+ self.authenticate()
788
1383
 
789
1384
  request_header = self.request_header()
790
- request_url = f"{self._instance_url}/services/data/v52.0/sobjects/Account/"
1385
+ request_url = self.config()["accountUrl"]
791
1386
 
792
1387
  payload = {
793
1388
  "Name": account_name,
@@ -800,33 +1395,23 @@ class Salesforce(object):
800
1395
  }
801
1396
  payload.update(kwargs) # Add additional fields from kwargs
802
1397
 
803
- logger.info(
804
- "Adding Salesforce account -> %s; calling -> %s", account_name, request_url
1398
+ logger.debug(
1399
+ "Adding Salesforce account -> '%s' (%s); calling -> %s",
1400
+ account_name,
1401
+ account_number,
1402
+ request_url,
805
1403
  )
806
1404
 
807
- retries = 0
808
- while True:
809
- response = requests.post(
810
- request_url,
811
- headers=request_header,
812
- data=json.dumps(payload),
813
- timeout=REQUEST_TIMEOUT,
814
- )
815
- if response.ok:
816
- return self.parse_request_response(response)
817
- elif response.status_code == 401 and retries == 0:
818
- logger.warning("Session has expired - try to re-authenticate...")
819
- self.authenticate(revalidate=True)
820
- request_header = self.request_header()
821
- retries += 1
822
- else:
823
- logger.error(
824
- "Failed to add Salesforce account -> %s; status -> %s; error -> %s",
825
- account_name,
826
- response.status_code,
827
- response.text,
828
- )
829
- return None
1405
+ return self.do_request(
1406
+ method="POST",
1407
+ url=request_url,
1408
+ headers=request_header,
1409
+ data=json.dumps(payload),
1410
+ timeout=REQUEST_TIMEOUT,
1411
+ failure_message="Failed to add Salesforce account -> '{}' ({})".format(
1412
+ account_name, account_number
1413
+ ),
1414
+ )
830
1415
 
831
1416
  # end method definition
832
1417
 
@@ -851,11 +1436,10 @@ class Salesforce(object):
851
1436
  """
852
1437
 
853
1438
  if not self._access_token or not self._instance_url:
854
- logger.error("Authentication required.")
855
- return None
1439
+ self.authenticate()
856
1440
 
857
1441
  request_header = self.request_header()
858
- request_url = f"{self._instance_url}/services/data/v52.0/sobjects/Product2/"
1442
+ request_url = self.config()["productUrl"]
859
1443
 
860
1444
  payload = {
861
1445
  "Name": product_name,
@@ -865,33 +1449,23 @@ class Salesforce(object):
865
1449
  }
866
1450
  payload.update(kwargs) # Add additional fields from kwargs
867
1451
 
868
- logger.info(
869
- "Add Salesforce product -> %s; calling -> %s", product_name, request_url
1452
+ logger.debug(
1453
+ "Add Salesforce product -> '%s' (%s); calling -> %s",
1454
+ product_name,
1455
+ product_code,
1456
+ request_url,
870
1457
  )
871
1458
 
872
- retries = 0
873
- while True:
874
- response = requests.post(
875
- request_url,
876
- headers=request_header,
877
- data=json.dumps(payload),
878
- timeout=REQUEST_TIMEOUT,
879
- )
880
- if response.ok:
881
- return self.parse_request_response(response)
882
- elif response.status_code == 401 and retries == 0:
883
- logger.warning("Session has expired - try to re-authenticate...")
884
- self.authenticate(revalidate=True)
885
- request_header = self.request_header()
886
- retries += 1
887
- else:
888
- logger.error(
889
- "Failed to add Salesforce product -> %s; status -> %s; error -> %s",
890
- product_name,
891
- response.status_code,
892
- response.text,
893
- )
894
- return None
1459
+ return self.do_request(
1460
+ method="POST",
1461
+ url=request_url,
1462
+ headers=request_header,
1463
+ data=json.dumps(payload),
1464
+ timeout=REQUEST_TIMEOUT,
1465
+ failure_message="Failed to add Salesforce product -> '{}' ({})".format(
1466
+ product_name, product_code
1467
+ ),
1468
+ )
895
1469
 
896
1470
  # end method definition
897
1471
 
@@ -922,11 +1496,10 @@ class Salesforce(object):
922
1496
  """
923
1497
 
924
1498
  if not self._access_token or not self._instance_url:
925
- logger.error("Authentication required.")
926
- return None
1499
+ self.authenticate()
927
1500
 
928
1501
  request_header = self.request_header()
929
- request_url = f"{self._instance_url}/services/data/v52.0/sobjects/Opportunity/"
1502
+ request_url = self.config()["opportunityUrl"]
930
1503
 
931
1504
  payload = {
932
1505
  "Name": name,
@@ -939,33 +1512,20 @@ class Salesforce(object):
939
1512
  payload["Description"] = description
940
1513
  payload.update(kwargs) # Add additional fields from kwargs
941
1514
 
942
- logger.info(
943
- "Add Salesforce opportunity -> %s; calling -> %s", name, request_url
1515
+ logger.debug(
1516
+ "Add Salesforce opportunity -> '%s'; calling -> %s", name, request_url
944
1517
  )
945
1518
 
946
- retries = 0
947
- while True:
948
- response = requests.post(
949
- request_url,
950
- headers=request_header,
951
- data=json.dumps(payload),
952
- timeout=REQUEST_TIMEOUT,
953
- )
954
- if response.ok:
955
- return self.parse_request_response(response)
956
- elif response.status_code == 401 and retries == 0:
957
- logger.warning("Session has expired - try to re-authenticate...")
958
- self.authenticate(revalidate=True)
959
- request_header = self.request_header()
960
- retries += 1
961
- else:
962
- logger.error(
963
- "Failed to add Salesforce opportunity -> %s; status -> %s; error -> %s",
964
- name,
965
- response.status_code,
966
- response.text,
967
- )
968
- return None
1519
+ return self.do_request(
1520
+ method="POST",
1521
+ url=request_url,
1522
+ headers=request_header,
1523
+ data=json.dumps(payload),
1524
+ timeout=REQUEST_TIMEOUT,
1525
+ failure_message="Failed to add Salesforce opportunity -> '{}'".format(name),
1526
+ )
1527
+
1528
+ # end method definition
969
1529
 
970
1530
  def add_case(
971
1531
  self,
@@ -990,6 +1550,7 @@ class Salesforce(object):
990
1550
  priority (str): Priority of the case. Typical values: "High", "Medium", "Low".
991
1551
  origin (str): origin (source) of the case. Typical values: "Email", "Phone", "Web"
992
1552
  account_id (str): technical ID of the related Account
1553
+ owner_id (str): owner of the case
993
1554
  asset_id (str): technical ID of the related Asset
994
1555
  product_id (str): technical ID of the related Product
995
1556
  kwargs (Any): additional values (e.g. custom fields)
@@ -999,11 +1560,10 @@ class Salesforce(object):
999
1560
  """
1000
1561
 
1001
1562
  if not self._access_token or not self._instance_url:
1002
- logger.error("Authentication required.")
1003
- return None
1563
+ self.authenticate()
1004
1564
 
1005
1565
  request_header = self.request_header()
1006
- request_url = f"{self._instance_url}/services/data/v52.0/sobjects/Case/"
1566
+ request_url = self.config()["caseUrl"]
1007
1567
 
1008
1568
  payload = {
1009
1569
  "Subject": subject,
@@ -1021,31 +1581,16 @@ class Salesforce(object):
1021
1581
  payload["ProductId"] = product_id
1022
1582
  payload.update(kwargs) # Add additional fields from kwargs
1023
1583
 
1024
- logger.info("Add Salesforce case -> %s; calling -> %s", subject, request_url)
1584
+ logger.debug("Add Salesforce case -> '%s'; calling -> %s", subject, request_url)
1025
1585
 
1026
- retries = 0
1027
- while True:
1028
- response = requests.post(
1029
- request_url,
1030
- headers=request_header,
1031
- data=json.dumps(payload),
1032
- timeout=REQUEST_TIMEOUT,
1033
- )
1034
- if response.ok:
1035
- return self.parse_request_response(response)
1036
- elif response.status_code == 401 and retries == 0:
1037
- logger.warning("Session has expired - try to re-authenticate...")
1038
- self.authenticate(revalidate=True)
1039
- request_header = self.request_header()
1040
- retries += 1
1041
- else:
1042
- logger.error(
1043
- "Failed to add Salesforce case -> %s; status -> %s; error -> %s",
1044
- subject,
1045
- response.status_code,
1046
- response.text,
1047
- )
1048
- return None
1586
+ return self.do_request(
1587
+ method="POST",
1588
+ url=request_url,
1589
+ headers=request_header,
1590
+ data=json.dumps(payload),
1591
+ timeout=REQUEST_TIMEOUT,
1592
+ failure_message="Failed to add Salesforce case -> '{}'".format(subject),
1593
+ )
1049
1594
 
1050
1595
  # end method definition
1051
1596
 
@@ -1077,11 +1622,10 @@ class Salesforce(object):
1077
1622
  """
1078
1623
 
1079
1624
  if not self._access_token or not self._instance_url:
1080
- logger.error("Authentication required.")
1081
- return None
1625
+ self.authenticate()
1082
1626
 
1083
1627
  request_header = self.request_header()
1084
- request_url = f"{self._instance_url}/services/data/v52.0/sobjects/Asset/"
1628
+ request_url = self.config()["assetUrl"]
1085
1629
 
1086
1630
  payload = {
1087
1631
  "Name": asset_name,
@@ -1095,33 +1639,18 @@ class Salesforce(object):
1095
1639
  payload["Description"] = description
1096
1640
  payload.update(kwargs) # Add additional fields from kwargs
1097
1641
 
1098
- logger.info(
1099
- "Add Salesforce asset -> %s; calling -> %s", asset_name, request_url
1642
+ logger.debug(
1643
+ "Add Salesforce asset -> '%s'; calling -> %s", asset_name, request_url
1100
1644
  )
1101
1645
 
1102
- retries = 0
1103
- while True:
1104
- response = requests.post(
1105
- request_url,
1106
- headers=request_header,
1107
- data=json.dumps(payload),
1108
- timeout=REQUEST_TIMEOUT,
1109
- )
1110
- if response.ok:
1111
- return self.parse_request_response(response)
1112
- elif response.status_code == 401 and retries == 0:
1113
- logger.warning("Session has expired - try to re-authenticate...")
1114
- self.authenticate(revalidate=True)
1115
- request_header = self.request_header()
1116
- retries += 1
1117
- else:
1118
- logger.error(
1119
- "Failed to add Salesforce user -> %s; status -> %s; error -> %s",
1120
- asset_name,
1121
- response.status_code,
1122
- response.text,
1123
- )
1124
- return None
1646
+ return self.do_request(
1647
+ method="POST",
1648
+ url=request_url,
1649
+ headers=request_header,
1650
+ data=json.dumps(payload),
1651
+ timeout=REQUEST_TIMEOUT,
1652
+ failure_message="Failed to add Salesforce asset -> '{}'".format(asset_name),
1653
+ )
1125
1654
 
1126
1655
  # end method definition
1127
1656
 
@@ -1151,11 +1680,10 @@ class Salesforce(object):
1151
1680
  """
1152
1681
 
1153
1682
  if not self._access_token or not self._instance_url:
1154
- logger.error("Authentication required.")
1155
- return None
1683
+ self.authenticate()
1156
1684
 
1157
1685
  request_header = self.request_header()
1158
- request_url = f"{self._instance_url}/services/data/v52.0/sobjects/Contract/"
1686
+ request_url = self.config()["contractUrl"]
1159
1687
 
1160
1688
  payload = {
1161
1689
  "AccountId": account_id,
@@ -1169,34 +1697,21 @@ class Salesforce(object):
1169
1697
  payload["ContractType"] = contract_type
1170
1698
  payload.update(kwargs) # Add additional fields from kwargs
1171
1699
 
1172
- logger.info(
1173
- "Adding Salesforce contract for account ID -> %s; calling -> %s",
1700
+ logger.debug(
1701
+ "Adding Salesforce contract for account with ID -> %s; calling -> %s",
1174
1702
  account_id,
1175
1703
  request_url,
1176
1704
  )
1177
1705
 
1178
- retries = 0
1179
- while True:
1180
- response = requests.post(
1181
- request_url,
1182
- headers=request_header,
1183
- data=json.dumps(payload),
1184
- timeout=REQUEST_TIMEOUT,
1185
- )
1186
- if response.ok:
1187
- return self.parse_request_response(response)
1188
- elif response.status_code == 401 and retries == 0:
1189
- logger.warning("Session has expired - try to re-authenticate...")
1190
- self.authenticate(revalidate=True)
1191
- request_header = self.request_header()
1192
- retries += 1
1193
- else:
1194
- logger.error(
1195
- "Failed to add Salesforce contract for account ID -> %s; status -> %s; error -> %s",
1196
- account_id,
1197
- response.status_code,
1198
- response.text,
1199
- )
1200
- return None
1706
+ return self.do_request(
1707
+ method="POST",
1708
+ url=request_url,
1709
+ headers=request_header,
1710
+ data=json.dumps(payload),
1711
+ timeout=REQUEST_TIMEOUT,
1712
+ failure_message="Failed to add Salesforce contract for account with ID -> {}".format(
1713
+ account_id
1714
+ ),
1715
+ )
1201
1716
 
1202
1717
  # end method definition