salespyforce 1.4.0.dev1__py3-none-any.whl → 1.4.0rc0__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.
- salespyforce/api.py +98 -14
- salespyforce/core.py +377 -15
- salespyforce/decorators.py +53 -0
- salespyforce/errors/handlers.py +40 -2
- salespyforce/knowledge.py +28 -5
- salespyforce/utils/core_utils.py +86 -2
- salespyforce/utils/log_utils.py +7 -7
- salespyforce/utils/tests/test_core_utils.py +40 -1
- salespyforce/utils/tests/test_log_utils.py +2 -2
- {salespyforce-1.4.0.dev1.dist-info → salespyforce-1.4.0rc0.dist-info}/METADATA +2 -10
- {salespyforce-1.4.0.dev1.dist-info → salespyforce-1.4.0rc0.dist-info}/RECORD +13 -12
- {salespyforce-1.4.0.dev1.dist-info → salespyforce-1.4.0rc0.dist-info}/LICENSE +0 -0
- {salespyforce-1.4.0.dev1.dist-info → salespyforce-1.4.0rc0.dist-info}/WHEEL +0 -0
salespyforce/api.py
CHANGED
|
@@ -4,12 +4,13 @@
|
|
|
4
4
|
:Synopsis: Defines the basic functions associated with the Salesforce API
|
|
5
5
|
:Created By: Jeff Shurtliff
|
|
6
6
|
:Last Modified: Jeff Shurtliff
|
|
7
|
-
:Modified Date:
|
|
7
|
+
:Modified Date: 30 Jan 2026
|
|
8
8
|
"""
|
|
9
9
|
|
|
10
10
|
import requests
|
|
11
11
|
|
|
12
|
-
from .
|
|
12
|
+
from . import errors
|
|
13
|
+
from .utils import core_utils, log_utils
|
|
13
14
|
|
|
14
15
|
# Initialize logging
|
|
15
16
|
logger = log_utils.initialize_logging(__name__)
|
|
@@ -19,6 +20,10 @@ def get(sfdc_object, endpoint, params=None, headers=None, timeout=30, show_full_
|
|
|
19
20
|
"""This method performs a GET request against the Salesforce instance.
|
|
20
21
|
(`Reference <https://jereze.com/code/authentification-salesforce-rest-api-python/>`_)
|
|
21
22
|
|
|
23
|
+
.. version-changed:: 1.4.0
|
|
24
|
+
The full URL for the API call is now constructed prior to making the call.
|
|
25
|
+
The provided URL is also now evaluated to ensure it is a valid Salesforce URL.
|
|
26
|
+
|
|
22
27
|
:param sfdc_object: The instantiated SalesPyForce object
|
|
23
28
|
:param endpoint: The API endpoint to query
|
|
24
29
|
:type endpoint: str
|
|
@@ -40,12 +45,13 @@ def get(sfdc_object, endpoint, params=None, headers=None, timeout=30, show_full_
|
|
|
40
45
|
default_headers = _get_headers(sfdc_object.access_token)
|
|
41
46
|
headers = default_headers if not headers else headers
|
|
42
47
|
|
|
43
|
-
#
|
|
44
|
-
|
|
48
|
+
# Construct the request URL
|
|
49
|
+
url = _construct_full_query_url(endpoint, sfdc_object.instance_url)
|
|
45
50
|
|
|
46
51
|
# Perform the API call
|
|
47
|
-
response = requests.get(
|
|
52
|
+
response = requests.get(url, headers=headers, params=params, timeout=timeout)
|
|
48
53
|
if response.status_code >= 300:
|
|
54
|
+
# TODO: Functionalize this segment and figure out how to improve on the approach somehow
|
|
49
55
|
if show_full_error:
|
|
50
56
|
raise RuntimeError(f'The GET request failed with a {response.status_code} status code.\n'
|
|
51
57
|
f'{response.text}')
|
|
@@ -61,6 +67,10 @@ def api_call_with_payload(sfdc_object, method, endpoint, payload, params=None, h
|
|
|
61
67
|
"""This method performs a POST call against the Salesforce instance.
|
|
62
68
|
(`Reference <https://jereze.com/code/authentification-salesforce-rest-api-python/>`_)
|
|
63
69
|
|
|
70
|
+
.. version-changed:: 1.4.0
|
|
71
|
+
The full URL for the API call is now constructed prior to making the call.
|
|
72
|
+
The provided URL is also now evaluated to ensure it is a valid Salesforce URL.
|
|
73
|
+
|
|
64
74
|
:param sfdc_object: The instantiated SalesPyForce object
|
|
65
75
|
:param method: The API method (``post``, ``put``, or ``patch``)
|
|
66
76
|
:type method: str
|
|
@@ -86,25 +96,23 @@ def api_call_with_payload(sfdc_object, method, endpoint, payload, params=None, h
|
|
|
86
96
|
default_headers = _get_headers(sfdc_object.access_token)
|
|
87
97
|
headers = default_headers if not headers else headers
|
|
88
98
|
|
|
89
|
-
#
|
|
90
|
-
|
|
99
|
+
# Construct the request URL
|
|
100
|
+
url = _construct_full_query_url(endpoint, sfdc_object.instance_url)
|
|
91
101
|
|
|
92
102
|
# Perform the API call
|
|
93
103
|
if method.lower() == 'post':
|
|
94
|
-
response = requests.post(
|
|
95
|
-
timeout=timeout)
|
|
104
|
+
response = requests.post(url, json=payload, headers=headers, params=params, timeout=timeout)
|
|
96
105
|
elif method.lower() == 'patch':
|
|
97
|
-
response = requests.patch(
|
|
98
|
-
timeout=timeout)
|
|
106
|
+
response = requests.patch(url, json=payload, headers=headers, params=params, timeout=timeout)
|
|
99
107
|
elif method.lower() == 'put':
|
|
100
|
-
response = requests.put(
|
|
101
|
-
timeout=timeout)
|
|
108
|
+
response = requests.put(url, json=payload, headers=headers, params=params, timeout=timeout)
|
|
102
109
|
else:
|
|
103
|
-
raise ValueError('The API call method (POST or PATCH
|
|
110
|
+
raise ValueError('The API call method (POST or PATCH or PUT) must be defined')
|
|
104
111
|
|
|
105
112
|
# Examine the result
|
|
106
113
|
if response.status_code >= 300:
|
|
107
114
|
if show_full_error:
|
|
115
|
+
# TODO: Functionalize this segment and figure out how to improve on the approach somehow
|
|
108
116
|
raise RuntimeError(f'The POST request failed with a {response.status_code} status code.\n'
|
|
109
117
|
f'{response.text}')
|
|
110
118
|
else:
|
|
@@ -117,6 +125,49 @@ def api_call_with_payload(sfdc_object, method, endpoint, payload, params=None, h
|
|
|
117
125
|
return response
|
|
118
126
|
|
|
119
127
|
|
|
128
|
+
def delete(sfdc_object, endpoint, params=None, headers=None, timeout=30, show_full_error=True, return_json=True):
|
|
129
|
+
"""This method performs a DELETE request against the Salesforce instance.
|
|
130
|
+
|
|
131
|
+
.. version-added:: 1.4.0
|
|
132
|
+
|
|
133
|
+
:param sfdc_object: The instantiated SalesPyForce object
|
|
134
|
+
:param endpoint: The API endpoint to query
|
|
135
|
+
:type endpoint: str
|
|
136
|
+
:param params: The query parameters (where applicable)
|
|
137
|
+
:type params: dict, None
|
|
138
|
+
:param headers: Specific API headers to use when performing the API call
|
|
139
|
+
:type headers: dict, None
|
|
140
|
+
:param timeout: The timeout period in seconds (defaults to ``30``)
|
|
141
|
+
:type timeout: int, str, None
|
|
142
|
+
:param show_full_error: Determines if the full error message should be displayed (defaults to ``True``)
|
|
143
|
+
:type show_full_error: bool
|
|
144
|
+
:param return_json: Determines if the response should be returned in JSON format (defaults to ``True``)
|
|
145
|
+
:returns: The API response in JSON format or as a ``requests`` object
|
|
146
|
+
"""
|
|
147
|
+
# Define the parameters as an empty dictionary if none are provided
|
|
148
|
+
params = {} if params is None else params
|
|
149
|
+
|
|
150
|
+
# Define the headers
|
|
151
|
+
default_headers = _get_headers(sfdc_object.access_token)
|
|
152
|
+
headers = default_headers if not headers else headers
|
|
153
|
+
|
|
154
|
+
# Construct the request URL
|
|
155
|
+
url = _construct_full_query_url(endpoint, sfdc_object.instance_url)
|
|
156
|
+
|
|
157
|
+
# Perform the API call
|
|
158
|
+
response = requests.delete(url, headers=headers, params=params, timeout=timeout)
|
|
159
|
+
if response.status_code >= 300:
|
|
160
|
+
if show_full_error:
|
|
161
|
+
# TODO: Functionalize this segment and figure out how to improve on the approach somehow
|
|
162
|
+
raise RuntimeError(f'The DELETE request failed with a {response.status_code} status code.\n'
|
|
163
|
+
f'{response.text}')
|
|
164
|
+
else:
|
|
165
|
+
raise RuntimeError(f'The DELETE request failed with a {response.status_code} status code.')
|
|
166
|
+
if return_json:
|
|
167
|
+
response = response.json()
|
|
168
|
+
return response
|
|
169
|
+
|
|
170
|
+
|
|
120
171
|
def _get_headers(_access_token, _header_type='default'):
|
|
121
172
|
"""This function returns the appropriate HTTP headers to use for different types of API calls."""
|
|
122
173
|
headers = {
|
|
@@ -127,3 +178,36 @@ def _get_headers(_access_token, _header_type='default'):
|
|
|
127
178
|
if _header_type == 'articles':
|
|
128
179
|
headers['accept-language'] = 'en-US'
|
|
129
180
|
return headers
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def _construct_full_query_url(_endpoint: str, _instance_url: str) -> str:
|
|
184
|
+
"""This function constructs the URL to use in an API call to the Salesforce REST API.
|
|
185
|
+
|
|
186
|
+
.. version-added:: 1.4.0
|
|
187
|
+
|
|
188
|
+
:param _endpoint: The endpoint provided when calling an API call method or function
|
|
189
|
+
:type _endpoint: str
|
|
190
|
+
:param _instance_url: The Salesforce instance URL defined when the core object was instantiated
|
|
191
|
+
:type _instance_url: str
|
|
192
|
+
:returns: The fully qualified URL
|
|
193
|
+
:raises: :py:exc:`TypeError`,
|
|
194
|
+
:py:exc:`errors.exceptions.InvalidURLError`
|
|
195
|
+
"""
|
|
196
|
+
# Raise an exception if the endpoint is not a string
|
|
197
|
+
if not isinstance(_endpoint, str):
|
|
198
|
+
exc_msg = 'The provided URL must be a string and a valid Salesforce URL'
|
|
199
|
+
logger.critical(exc_msg)
|
|
200
|
+
raise TypeError(exc_msg)
|
|
201
|
+
|
|
202
|
+
# Construct the URL as needed by prepending the instance URL
|
|
203
|
+
if _endpoint.startswith('https://'):
|
|
204
|
+
# Only permit valid Salesforce URLs
|
|
205
|
+
if not core_utils.is_valid_salesforce_url(_endpoint):
|
|
206
|
+
raise errors.exceptions.InvalidURLError(url=_endpoint)
|
|
207
|
+
_url = _endpoint
|
|
208
|
+
else:
|
|
209
|
+
_endpoint = f'/{_endpoint}' if not _endpoint.startswith('/') else _endpoint
|
|
210
|
+
_url = f'{_instance_url}{_endpoint}'
|
|
211
|
+
|
|
212
|
+
# Return the constructed URL
|
|
213
|
+
return _url
|
salespyforce/core.py
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
:Example: ``sfdc = Salesforce(helper=helper_file_path)``
|
|
7
7
|
:Created By: Jeff Shurtliff
|
|
8
8
|
:Last Modified: Jeff Shurtliff
|
|
9
|
-
:Modified Date:
|
|
9
|
+
:Modified Date: 03 Feb 2026
|
|
10
10
|
"""
|
|
11
11
|
|
|
12
12
|
import re
|
|
@@ -21,6 +21,7 @@ from .utils.helper import get_helper_settings
|
|
|
21
21
|
|
|
22
22
|
# Define constants
|
|
23
23
|
FALLBACK_SFDC_API_VERSION = '65.0' # Used if querying the org for the version fails
|
|
24
|
+
VALID_ACCESS_CONTROL_FIELDS = {'HasReadAccess', 'HasEditAccess', 'HasDeleteAccess'}
|
|
24
25
|
|
|
25
26
|
# Initialize logging
|
|
26
27
|
logger = log_utils.initialize_logging(__name__)
|
|
@@ -60,7 +61,8 @@ class Salesforce(object):
|
|
|
60
61
|
:param helper: The file path of a helper file
|
|
61
62
|
:type helper: str, None
|
|
62
63
|
:returns: The instantiated object
|
|
63
|
-
:raises: :py:exc:`TypeError
|
|
64
|
+
:raises: :py:exc:`TypeError`,
|
|
65
|
+
:py:exc:`RuntimeError`
|
|
64
66
|
"""
|
|
65
67
|
# Define the default settings
|
|
66
68
|
self._helper_settings = {}
|
|
@@ -94,8 +96,9 @@ class Salesforce(object):
|
|
|
94
96
|
# Get the connection information used to connect to the instance
|
|
95
97
|
self.connection_info = connection_info if connection_info is not None else self._get_empty_connection_info()
|
|
96
98
|
|
|
97
|
-
# Define the base URL
|
|
98
|
-
self.base_url = self.connection_info.get('base_url')
|
|
99
|
+
# Define the base URL and Org ID
|
|
100
|
+
self.base_url = self.connection_info.get('base_url', '')
|
|
101
|
+
self.org_id = self.connection_info.get('org_id', '')
|
|
99
102
|
|
|
100
103
|
# Define the connection response data variables
|
|
101
104
|
auth_response = self.connect()
|
|
@@ -106,6 +109,9 @@ class Salesforce(object):
|
|
|
106
109
|
# Define the version with explicitly provided version or by querying the Salesforce org
|
|
107
110
|
self.version = f'v{version}' if version else f'v{self.get_latest_api_version()}'
|
|
108
111
|
|
|
112
|
+
# Retrieve info about current user
|
|
113
|
+
self.current_user_info = self.retrieve_current_user_info(on_init=True, raise_exc_on_error=False)
|
|
114
|
+
|
|
109
115
|
# Import inner object classes so their methods can be called from the primary object
|
|
110
116
|
self.chatter = self._import_chatter_class()
|
|
111
117
|
self.knowledge = self._import_knowledge_class()
|
|
@@ -142,11 +148,40 @@ class Salesforce(object):
|
|
|
142
148
|
"""This method returns the appropriate HTTP headers to use for different types of API calls."""
|
|
143
149
|
return api._get_headers(_access_token=self.access_token, _header_type=_header_type)
|
|
144
150
|
|
|
151
|
+
def _get_cached_user_info(self, _field: str, _retrieve_if_missing: bool = False):
|
|
152
|
+
"""This method attempts to retrieve a value for a given field in the cached ``userinfo`` data and
|
|
153
|
+
optionally queries the API as needed to retrieve the data when not found.
|
|
154
|
+
|
|
155
|
+
.. version-added:: 1.4.0
|
|
156
|
+
|
|
157
|
+
:param _field: The name of the field for which the value is needed
|
|
158
|
+
:type _field: str
|
|
159
|
+
:param _retrieve_if_missing: Will query the Salesforce REST API for the data when missing if True
|
|
160
|
+
(``False`` by default)
|
|
161
|
+
:type _retrieve_if_missing: bool
|
|
162
|
+
:returns: The field value when found (or retrieved), or a None value if the field value could not be obtained
|
|
163
|
+
:raises: :py:exc:`errors.exceptions.APIRequestError`
|
|
164
|
+
"""
|
|
165
|
+
_field_value = None
|
|
166
|
+
_not_present_msg = f"The '{_field}' field is not present in the current user info data"
|
|
167
|
+
if self.current_user_info and _field in self.current_user_info:
|
|
168
|
+
_field_value = self.current_user_info[_field]
|
|
169
|
+
else:
|
|
170
|
+
logger.warning(_not_present_msg)
|
|
171
|
+
if _retrieve_if_missing:
|
|
172
|
+
self.current_user_info = self.retrieve_current_user_info(raise_exc_on_error=True)
|
|
173
|
+
if self.current_user_info and _field in self.current_user_info:
|
|
174
|
+
_field_value = self.current_user_info[_field]
|
|
175
|
+
else:
|
|
176
|
+
logger.error(f'{_not_present_msg} even after refreshing cached current user info')
|
|
177
|
+
return _field_value
|
|
178
|
+
|
|
145
179
|
def connect(self):
|
|
146
180
|
"""This method connects to the Salesforce instance to obtain the access token.
|
|
147
181
|
(`Reference <https://jereze.com/code/authentification-salesforce-rest-api-python/>`_)
|
|
148
182
|
|
|
149
183
|
:returns: The API call response with the authorization information
|
|
184
|
+
:raises: :py:exc:`RuntimeError`
|
|
150
185
|
"""
|
|
151
186
|
params = {
|
|
152
187
|
'grant_type': 'password',
|
|
@@ -160,6 +195,51 @@ class Salesforce(object):
|
|
|
160
195
|
raise RuntimeError(f'Failed to connect to the Salesforce instance.\n{response.text}')
|
|
161
196
|
return response.json()
|
|
162
197
|
|
|
198
|
+
def retrieve_current_user_info(self, all_data=False, raise_exc_on_error=False, on_init=False) -> dict:
|
|
199
|
+
"""This method retrieves the ``userinfo`` data for the current/running user.
|
|
200
|
+
|
|
201
|
+
.. version-added:: 1.4.0
|
|
202
|
+
|
|
203
|
+
:param all_data: Returns all ``userinfo`` data from the API when True instead of only the relevant fields/values
|
|
204
|
+
(``False`` by default)
|
|
205
|
+
:type all_data: bool
|
|
206
|
+
:param raise_exc_on_error: Raises an exception if the API retrieval attempt fails when True (``False`` by default)
|
|
207
|
+
:type raise_exc_on_error: bool
|
|
208
|
+
:param on_init: Indicates if the method is being called during the core object instantiation (``False`` by default)
|
|
209
|
+
:type on_init: bool
|
|
210
|
+
:returns: The user info data within a dictionary
|
|
211
|
+
:raises: :py:exc:`RuntimeError`,
|
|
212
|
+
:py:exc:`errors.exceptions.APIRequestError`
|
|
213
|
+
"""
|
|
214
|
+
user_info = {'user_id': '', 'nickname': '', 'name': '', 'email': '', 'user_type': '',
|
|
215
|
+
'language': '', 'locale': '', 'utcOffset': '', 'is_salesforce_integration_user': None}
|
|
216
|
+
bool_fields = ['is_salesforce_integration_user']
|
|
217
|
+
endpoint = '/services/oauth2/userinfo'
|
|
218
|
+
base_error_msg = 'Failed to retrieve current user info'
|
|
219
|
+
msg_init_segment = 'on core object instantiation'
|
|
220
|
+
if on_init:
|
|
221
|
+
base_error_msg = f'{base_error_msg} {msg_init_segment}'
|
|
222
|
+
try:
|
|
223
|
+
response = self.get(endpoint)
|
|
224
|
+
if isinstance(response, dict) and all_data:
|
|
225
|
+
user_info = response
|
|
226
|
+
elif isinstance(response, dict):
|
|
227
|
+
for field in user_info.keys():
|
|
228
|
+
if field in response:
|
|
229
|
+
default_val = None if field in bool_fields else ''
|
|
230
|
+
user_info[field] = response.get(field, default_val)
|
|
231
|
+
else:
|
|
232
|
+
logger.error(f'{base_error_msg} with a usable format')
|
|
233
|
+
except Exception as exc:
|
|
234
|
+
exc_type = errors.handlers.get_exception_type(exc)
|
|
235
|
+
exc_msg = f'{base_error_msg} due to {exc_type} exception: {exc}'
|
|
236
|
+
logger.error(exc_msg)
|
|
237
|
+
if raise_exc_on_error:
|
|
238
|
+
raise errors.exceptions.APIRequestError(f'{exc_type}: {exc}')
|
|
239
|
+
|
|
240
|
+
# Return the populated user info
|
|
241
|
+
return user_info
|
|
242
|
+
|
|
163
243
|
def get(self, endpoint, params=None, headers=None, timeout=30, show_full_error=True, return_json=True):
|
|
164
244
|
"""This method performs a GET request against the Salesforce instance.
|
|
165
245
|
(`Reference <https://jereze.com/code/authentification-salesforce-rest-api-python/>`_)
|
|
@@ -176,6 +256,7 @@ class Salesforce(object):
|
|
|
176
256
|
:type show_full_error: bool
|
|
177
257
|
:param return_json: Determines if the response should be returned in JSON format (defaults to ``True``)
|
|
178
258
|
:returns: The API response in JSON format or as a ``requests`` object
|
|
259
|
+
:raises: :py:exc:`RuntimeError`
|
|
179
260
|
"""
|
|
180
261
|
return api.get(self, endpoint=endpoint, params=params, headers=headers, timeout=timeout,
|
|
181
262
|
show_full_error=show_full_error, return_json=return_json)
|
|
@@ -201,6 +282,7 @@ class Salesforce(object):
|
|
|
201
282
|
:type show_full_error: bool
|
|
202
283
|
:param return_json: Determines if the response should be returned in JSON format (defaults to ``True``)
|
|
203
284
|
:returns: The API response in JSON format or as a ``requests`` object
|
|
285
|
+
:raises: :py:exc:`RuntimeError`
|
|
204
286
|
"""
|
|
205
287
|
return api.api_call_with_payload(self, method=method, endpoint=endpoint, payload=payload, params=params,
|
|
206
288
|
headers=headers, timeout=timeout, show_full_error=show_full_error,
|
|
@@ -224,6 +306,7 @@ class Salesforce(object):
|
|
|
224
306
|
:type show_full_error: bool
|
|
225
307
|
:param return_json: Determines if the response should be returned in JSON format (defaults to ``True``)
|
|
226
308
|
:returns: The API response in JSON format or as a ``requests`` object
|
|
309
|
+
:raises: :py:exc:`RuntimeError`
|
|
227
310
|
"""
|
|
228
311
|
return api.api_call_with_payload(self, 'post', endpoint=endpoint, payload=payload, params=params,
|
|
229
312
|
headers=headers, timeout=timeout, show_full_error=show_full_error,
|
|
@@ -247,6 +330,7 @@ class Salesforce(object):
|
|
|
247
330
|
:type show_full_error: bool
|
|
248
331
|
:param return_json: Determines if the response should be returned in JSON format (defaults to ``True``)
|
|
249
332
|
:returns: The API response in JSON format or as a ``requests`` object
|
|
333
|
+
:raises: :py:exc:`RuntimeError`
|
|
250
334
|
"""
|
|
251
335
|
return api.api_call_with_payload(self, 'patch', endpoint=endpoint, payload=payload, params=params,
|
|
252
336
|
headers=headers, timeout=timeout, show_full_error=show_full_error,
|
|
@@ -270,16 +354,41 @@ class Salesforce(object):
|
|
|
270
354
|
:type show_full_error: bool
|
|
271
355
|
:param return_json: Determines if the response should be returned in JSON format (defaults to ``True``)
|
|
272
356
|
:returns: The API response in JSON format or as a ``requests`` object
|
|
357
|
+
:raises: :py:exc:`RuntimeError`
|
|
273
358
|
"""
|
|
274
359
|
return api.api_call_with_payload(self, 'put', endpoint=endpoint, payload=payload, params=params,
|
|
275
360
|
headers=headers, timeout=timeout, show_full_error=show_full_error,
|
|
276
361
|
return_json=return_json)
|
|
277
362
|
|
|
363
|
+
def delete(self, endpoint, params=None, headers=None, timeout=30, show_full_error=True, return_json=True):
|
|
364
|
+
"""This method performs a DELETE request against the Salesforce instance.
|
|
365
|
+
(`Reference <https://jereze.com/code/authentification-salesforce-rest-api-python/>`_)
|
|
366
|
+
|
|
367
|
+
.. version-added:: 1.4.0
|
|
368
|
+
|
|
369
|
+
:param endpoint: The API endpoint to query
|
|
370
|
+
:type endpoint: str
|
|
371
|
+
:param params: The query parameters (where applicable)
|
|
372
|
+
:type params: dict, None
|
|
373
|
+
:param headers: Specific API headers to use when performing the API call
|
|
374
|
+
:type headers: dict, None
|
|
375
|
+
:param timeout: The timeout period in seconds (defaults to ``30``)
|
|
376
|
+
:type timeout: int, str, None
|
|
377
|
+
:param show_full_error: Determines if the full error message should be displayed (defaults to ``True``)
|
|
378
|
+
:type show_full_error: bool
|
|
379
|
+
:param return_json: Determines if the response should be returned in JSON format (defaults to ``True``)
|
|
380
|
+
:returns: The API response in JSON format or as a ``requests`` object
|
|
381
|
+
:raises: :py:exc:`RuntimeError`
|
|
382
|
+
"""
|
|
383
|
+
return api.delete(self, endpoint=endpoint, params=params, headers=headers, timeout=timeout,
|
|
384
|
+
show_full_error=show_full_error, return_json=return_json)
|
|
385
|
+
|
|
278
386
|
def get_api_versions(self) -> list:
|
|
279
387
|
"""This method returns the API versions for the Salesforce releases.
|
|
280
388
|
(`Reference <https://developer.salesforce.com/docs/atlas.en-us.api_rest.meta/api_rest/dome_versions.htm>`_)
|
|
281
389
|
|
|
282
390
|
:returns: A list containing the API metadata from the ``/services/data`` endpoint.
|
|
391
|
+
:raises: :py:exc:`RuntimeError`
|
|
283
392
|
"""
|
|
284
393
|
return self.get('/services/data')
|
|
285
394
|
|
|
@@ -305,13 +414,19 @@ class Salesforce(object):
|
|
|
305
414
|
def get_org_limits(self):
|
|
306
415
|
"""This method returns a list of all org limits.
|
|
307
416
|
|
|
308
|
-
..
|
|
417
|
+
.. version-added:: 1.1.0
|
|
418
|
+
|
|
419
|
+
:returns: The Salesforce org governor limits data
|
|
420
|
+
:raises: :py:exc:`RuntimeError`
|
|
309
421
|
"""
|
|
310
422
|
return self.get(f'/services/data/{self.version}/limits')
|
|
311
423
|
|
|
312
424
|
def get_all_sobjects(self):
|
|
313
425
|
"""This method returns a list of all Salesforce objects. (i.e. sObjects)
|
|
314
426
|
(`Reference <https://developer.salesforce.com/docs/atlas.en-us.api_rest.meta/api_rest/dome_describeGlobal.htm>`_)
|
|
427
|
+
|
|
428
|
+
:returns: The list of all Salesforce objects
|
|
429
|
+
:raises: :py:exc:`RuntimeError`
|
|
315
430
|
"""
|
|
316
431
|
return self.get(f'/services/data/{self.version}/sobjects')
|
|
317
432
|
|
|
@@ -325,6 +440,7 @@ class Salesforce(object):
|
|
|
325
440
|
:param describe: Determines if the full (i.e. ``describe``) data should be returned (defaults to ``False``)
|
|
326
441
|
:type describe: bool
|
|
327
442
|
:returns: The Salesforce object data
|
|
443
|
+
:raises: :py:exc:`RuntimeError`
|
|
328
444
|
"""
|
|
329
445
|
uri = f'/services/data/{self.version}/sobjects/{object_name}'
|
|
330
446
|
uri = f'{uri}/describe' if describe else uri
|
|
@@ -337,15 +453,32 @@ class Salesforce(object):
|
|
|
337
453
|
:param object_name: The name of the Salesforce object
|
|
338
454
|
:type object_name: str
|
|
339
455
|
:returns: The Salesforce object data
|
|
456
|
+
:raises: :py:exc:`RuntimeError`
|
|
340
457
|
"""
|
|
341
458
|
return self.get_sobject(object_name, describe=True)
|
|
342
459
|
|
|
343
460
|
def get_rest_resources(self):
|
|
344
461
|
"""This method returns a list of all available REST resources.
|
|
345
462
|
(`Reference <https://developer.salesforce.com/docs/atlas.en-us.api_rest.meta/api_rest/dome_discoveryresource.htm>`_)
|
|
463
|
+
|
|
464
|
+
:returns: The list of all available REST resources for the Salesforce org
|
|
465
|
+
:raises: :py:exc:`RuntimeError`
|
|
346
466
|
"""
|
|
347
467
|
return self.get(f'/services/data/{self.version}')
|
|
348
468
|
|
|
469
|
+
@staticmethod
|
|
470
|
+
def get_18_char_id(record_id: str) -> str:
|
|
471
|
+
"""This method converts a 15-character Salesforce record ID to its 18-character case-insensitive form.
|
|
472
|
+
|
|
473
|
+
.. version-added:: 1.4.0
|
|
474
|
+
|
|
475
|
+
:param record_id: The Salesforce record ID to convert (or return unchanged if already 18 characters)
|
|
476
|
+
:type record_id: str
|
|
477
|
+
:returns: The 18-character Salesforce record ID
|
|
478
|
+
:raises: :py:exc:`ValueError`
|
|
479
|
+
"""
|
|
480
|
+
return core_utils.get_18_char_id(record_id=record_id)
|
|
481
|
+
|
|
349
482
|
def soql_query(self, query, replace_quotes=True, next_records_url=False):
|
|
350
483
|
"""This method performs a SOQL query and returns the results in JSON format.
|
|
351
484
|
(`Reference 1 <https://developer.salesforce.com/docs/atlas.en-us.api_rest.meta/api_rest/dome_query.htm>`_,
|
|
@@ -358,6 +491,7 @@ class Salesforce(object):
|
|
|
358
491
|
:param next_records_url: Indicates that the ``query`` parameter is a ``nextRecordsUrl`` value.
|
|
359
492
|
:type next_records_url: bool
|
|
360
493
|
:returns: The result of the SOQL query
|
|
494
|
+
:raises: :py:exc:`RuntimeError`
|
|
361
495
|
"""
|
|
362
496
|
if next_records_url:
|
|
363
497
|
query = re.sub(r'^.*/', '', query) if '/' in query else query
|
|
@@ -372,16 +506,215 @@ class Salesforce(object):
|
|
|
372
506
|
"""This method performs a SOSL query to search for a given string.
|
|
373
507
|
(`Reference <https://developer.salesforce.com/docs/atlas.en-us.api_rest.meta/api_rest/resources_search.htm>`_)
|
|
374
508
|
|
|
375
|
-
..
|
|
509
|
+
.. version-added:: 1.1.0
|
|
376
510
|
|
|
377
511
|
:param string_to_search: The string for which to search
|
|
378
512
|
:type string_to_search: str
|
|
379
513
|
:returns: The SOSL response data in JSON format
|
|
514
|
+
:raises: :py:exc:`RuntimeError`
|
|
380
515
|
"""
|
|
381
516
|
query = 'FIND {' + string_to_search + '}'
|
|
382
517
|
query = core_utils.url_encode(query)
|
|
383
518
|
return self.get(f'/services/data/{self.version}/search/?q={query}')
|
|
384
519
|
|
|
520
|
+
def check_user_record_access(self, record_id: str, user_id=None) -> dict:
|
|
521
|
+
"""This method checks the Read, Edit, and Delete access for a given record and user.
|
|
522
|
+
|
|
523
|
+
.. version-added:: 1.4.0
|
|
524
|
+
|
|
525
|
+
:param record_id: The ``Id`` value of the record against which to check the user access
|
|
526
|
+
:type record_id: str
|
|
527
|
+
:param user_id: The ``Id`` of the user to evaluate (or the current user's ID if not explicitly defined)
|
|
528
|
+
:type user_id: str, None
|
|
529
|
+
:returns: Dictionary with Boolean values for ``HasReadAccess``, ``HasEditAccess``, and ``HasDeleteAccess``
|
|
530
|
+
:raises: :py:exc:`RuntimeError`,
|
|
531
|
+
:py:exc:`errors.exceptions.APIRequestError`
|
|
532
|
+
"""
|
|
533
|
+
record_access = {'HasReadAccess': None, 'HasEditAccess': None, 'HasDeleteAccess': None}
|
|
534
|
+
|
|
535
|
+
# Use the current/running user's ID if an ID wasn't explicitly provided
|
|
536
|
+
if not user_id:
|
|
537
|
+
user_id = self._get_cached_user_info(_field='user_id', _retrieve_if_missing=True)
|
|
538
|
+
if user_id:
|
|
539
|
+
logger.debug(f'Using the User Id {user_id} for the running user as an Id was not specified')
|
|
540
|
+
|
|
541
|
+
# Raise an exception if the User ID is still undefined
|
|
542
|
+
if not user_id:
|
|
543
|
+
error_msg = f'The user access for record Id {record_id} cannot be checked as the User Id is undefined'
|
|
544
|
+
logger.error(error_msg)
|
|
545
|
+
raise errors.exceptions.MissingRequiredDataError(error_msg)
|
|
546
|
+
|
|
547
|
+
# Perform SOQL query for the access data
|
|
548
|
+
query = f"""
|
|
549
|
+
SELECT RecordId, HasReadAccess, HasEditAccess, HasDeleteAccess
|
|
550
|
+
FROM UserRecordAccess
|
|
551
|
+
WHERE UserId = '{user_id}' AND RecordId = '{record_id}'
|
|
552
|
+
"""
|
|
553
|
+
response = self.soql_query(query=query)
|
|
554
|
+
|
|
555
|
+
# Parse the response to extract the relevant field values
|
|
556
|
+
if 'records' in response and response['records']:
|
|
557
|
+
response = response['records'][0]
|
|
558
|
+
for field in record_access.keys():
|
|
559
|
+
record_access[field] = response.get(field, None)
|
|
560
|
+
|
|
561
|
+
# Return the record access data
|
|
562
|
+
return record_access
|
|
563
|
+
|
|
564
|
+
@staticmethod
|
|
565
|
+
def _eval_user_record_access(_field: str, _record_id: str,
|
|
566
|
+
_record_access_data: dict, _raise_exc_on_failure: bool = True) -> bool:
|
|
567
|
+
"""This private method checks for an access level given the field and the record access data.
|
|
568
|
+
|
|
569
|
+
.. version-added:: 1.4.0
|
|
570
|
+
|
|
571
|
+
:param _field: The access level field to evaluate (``HasReadAccess``, ``HasEditAccess``, ``HasDeleteAccess``)
|
|
572
|
+
:type _field: str
|
|
573
|
+
:param _record_id: The ID value for the record whose access is being checked
|
|
574
|
+
:type _record_id: str
|
|
575
|
+
:param _record_access_data: The user record access data that has already been retrieved
|
|
576
|
+
:type _record_access_data: dict
|
|
577
|
+
:param _raise_exc_on_failure: Raises an exception rather than returning a ``None`` value (``True`` by default)
|
|
578
|
+
:type _raise_exc_on_failure: bool
|
|
579
|
+
:returns: Boolean value indicating the access level for the given field
|
|
580
|
+
:raises: :py:exc:`errors.exceptions.InvalidFieldError`
|
|
581
|
+
"""
|
|
582
|
+
# Raise an exception if a valid access control field is not provided
|
|
583
|
+
if _field not in VALID_ACCESS_CONTROL_FIELDS:
|
|
584
|
+
_error_msg = f"The field '{_field}' is not a valid record access level field"
|
|
585
|
+
raise errors.exceptions.InvalidFieldError(_error_msg)
|
|
586
|
+
|
|
587
|
+
# Identify the access level value if possible (API retrievals should be handled in a parent method)
|
|
588
|
+
_has_access = _record_access_data.get(_field) if _field in _record_access_data else None
|
|
589
|
+
if _has_access is None:
|
|
590
|
+
_error_msg = f"The value for the '{_field}' is undefined for the given Record Id '{_record_id}'"
|
|
591
|
+
logger.error(_error_msg)
|
|
592
|
+
if _raise_exc_on_failure:
|
|
593
|
+
raise errors.exceptions.MissingRequiredDataError(_error_msg)
|
|
594
|
+
|
|
595
|
+
# Return the identified access level value
|
|
596
|
+
return _has_access
|
|
597
|
+
|
|
598
|
+
def can_access_record(self, access_type, record_id, user_id=None, record_access_data=None, raise_exc_on_failure=True):
|
|
599
|
+
"""This method evaluates if a user can access a specific record given the access type.
|
|
600
|
+
|
|
601
|
+
.. version-added:: 1.4.0
|
|
602
|
+
|
|
603
|
+
:param access_type: The type of access to evaluate (``read``, ``edit``, or ``delete``)
|
|
604
|
+
:type access_type: str
|
|
605
|
+
:param record_id: The ID of the record
|
|
606
|
+
:type record_id: str
|
|
607
|
+
:param user_id: The ID of the user to evaluate (defaults to the current/running user if not defined)
|
|
608
|
+
:type user_id: str, None
|
|
609
|
+
:param record_access_data: The user record access data that has already been retrieved (optional)
|
|
610
|
+
:type record_access_data: dict, None
|
|
611
|
+
:param raise_exc_on_failure: Raises an exception rather than returning a ``None`` value (``True`` by default)
|
|
612
|
+
:type raise_exc_on_failure: bool
|
|
613
|
+
:returns: Boolean value indicating the access level for the given field
|
|
614
|
+
:raises: :py:exc:`errors.exceptions.InvalidFieldError`
|
|
615
|
+
"""
|
|
616
|
+
# Define the initial value for the result
|
|
617
|
+
can_access = None
|
|
618
|
+
|
|
619
|
+
# Identify the correct field to query based on access type
|
|
620
|
+
access_type_field_mapping = {
|
|
621
|
+
'read': 'HasReadAccess',
|
|
622
|
+
'edit': 'HasEditAccess',
|
|
623
|
+
'delete': 'HasDeleteAccess',
|
|
624
|
+
}
|
|
625
|
+
if access_type.lower() not in access_type_field_mapping:
|
|
626
|
+
error_msg = f"The access_type '{access_type}' is invalid (must use 'read', 'edit', or 'delete')"
|
|
627
|
+
logger.error(error_msg)
|
|
628
|
+
if raise_exc_on_failure:
|
|
629
|
+
raise errors.exceptions.InvalidParameterError(error_msg)
|
|
630
|
+
else:
|
|
631
|
+
# Retrieve the field name to check
|
|
632
|
+
read_access_field = access_type_field_mapping.get(access_type.lower())
|
|
633
|
+
|
|
634
|
+
# Check to see if record access data was provided and validate that it is a dictionary
|
|
635
|
+
if record_access_data and not isinstance(record_access_data, dict):
|
|
636
|
+
error_msg = f"The record_access_data provided is Type {type(read_access_field)} but must be a dict"
|
|
637
|
+
logger.error(error_msg)
|
|
638
|
+
if raise_exc_on_failure:
|
|
639
|
+
raise errors.exceptions.DataMismatchError(error_msg)
|
|
640
|
+
|
|
641
|
+
# Perform the API all to check the record access for the user if data not provided
|
|
642
|
+
if not record_access_data:
|
|
643
|
+
record_access_data = self.check_user_record_access(record_id=record_id, user_id=user_id)
|
|
644
|
+
|
|
645
|
+
# Return the access level value
|
|
646
|
+
can_access = self._eval_user_record_access(_field=read_access_field, _record_id=record_id,
|
|
647
|
+
_record_access_data=record_access_data,
|
|
648
|
+
_raise_exc_on_failure=raise_exc_on_failure)
|
|
649
|
+
|
|
650
|
+
# Emit a warning if the value is None rather than a boolean
|
|
651
|
+
if can_access is None:
|
|
652
|
+
warn_msg = 'The record access check could not be completed and the function will return a None value'
|
|
653
|
+
errors.handlers.display_warning(warn_msg)
|
|
654
|
+
|
|
655
|
+
# Return the result
|
|
656
|
+
return can_access
|
|
657
|
+
|
|
658
|
+
def can_read_record(self, record_id, user_id=None, record_access_data=None, raise_exc_on_failure=True):
|
|
659
|
+
"""This method evaluates if a user has access to read a specific record.
|
|
660
|
+
|
|
661
|
+
.. version-added:: 1.4.0
|
|
662
|
+
|
|
663
|
+
:param record_id: The ID of the record
|
|
664
|
+
:type record_id: str
|
|
665
|
+
:param user_id: The ID of the user to evaluate (defaults to the current/running user if not defined)
|
|
666
|
+
:type user_id: str, None
|
|
667
|
+
:param record_access_data: The user record access data that has already been retrieved (optional)
|
|
668
|
+
:type record_access_data: dict, None
|
|
669
|
+
:param raise_exc_on_failure: Raises an exception rather than returning a ``None`` value (``True`` by default)
|
|
670
|
+
:type raise_exc_on_failure: bool
|
|
671
|
+
:returns: Boolean value indicating the access level for the given field
|
|
672
|
+
:raises: :py:exc:`errors.exceptions.InvalidFieldError`
|
|
673
|
+
"""
|
|
674
|
+
return self.can_access_record(access_type='read', record_id=record_id, user_id=user_id,
|
|
675
|
+
record_access_data=record_access_data,
|
|
676
|
+
raise_exc_on_failure=raise_exc_on_failure)
|
|
677
|
+
|
|
678
|
+
def can_edit_record(self, record_id, user_id=None, record_access_data=None, raise_exc_on_failure=True):
|
|
679
|
+
"""This method evaluates if a user has access to edit a specific record.
|
|
680
|
+
|
|
681
|
+
.. version-added:: 1.4.0
|
|
682
|
+
|
|
683
|
+
:param record_id: The ID of the record
|
|
684
|
+
:type record_id: str
|
|
685
|
+
:param user_id: The ID of the user to evaluate (defaults to the current/running user if not defined)
|
|
686
|
+
:type user_id: str, None
|
|
687
|
+
:param record_access_data: The user record access data that has already been retrieved (optional)
|
|
688
|
+
:type record_access_data: dict, None
|
|
689
|
+
:param raise_exc_on_failure: Raises an exception rather than returning a ``None`` value (``True`` by default)
|
|
690
|
+
:type raise_exc_on_failure: bool
|
|
691
|
+
:returns: Boolean value indicating the access level for the given field
|
|
692
|
+
:raises: :py:exc:`errors.exceptions.InvalidFieldError`
|
|
693
|
+
"""
|
|
694
|
+
return self.can_access_record(access_type='edit', record_id=record_id, user_id=user_id,
|
|
695
|
+
record_access_data=record_access_data,
|
|
696
|
+
raise_exc_on_failure=raise_exc_on_failure)
|
|
697
|
+
|
|
698
|
+
def can_delete_record(self, record_id, user_id=None, record_access_data=None, raise_exc_on_failure=True):
|
|
699
|
+
"""This method evaluates if a user has access to delete a specific record.
|
|
700
|
+
|
|
701
|
+
.. version-added:: 1.4.0
|
|
702
|
+
|
|
703
|
+
:param record_id: The ID of the record
|
|
704
|
+
:type record_id: str
|
|
705
|
+
:param user_id: The ID of the user to evaluate (defaults to the current/running user if not defined)
|
|
706
|
+
:type user_id: str, None
|
|
707
|
+
:param record_access_data: The user record access data that has already been retrieved (optional)
|
|
708
|
+
:type record_access_data: dict, None
|
|
709
|
+
:param raise_exc_on_failure: Raises an exception rather than returning a ``None`` value (``True`` by default)
|
|
710
|
+
:type raise_exc_on_failure: bool
|
|
711
|
+
:returns: Boolean value indicating the access level for the given field
|
|
712
|
+
:raises: :py:exc:`errors.exceptions.InvalidFieldError`
|
|
713
|
+
"""
|
|
714
|
+
return self.can_access_record(access_type='edit', record_id=record_id, user_id=user_id,
|
|
715
|
+
record_access_data=record_access_data,
|
|
716
|
+
raise_exc_on_failure=raise_exc_on_failure)
|
|
717
|
+
|
|
385
718
|
def create_sobject_record(self, sobject, payload):
|
|
386
719
|
"""This method creates a new record for a specific sObject.
|
|
387
720
|
(`Reference <https://developer.salesforce.com/docs/atlas.en-us.api_rest.meta/api_rest/dome_sobject_create.htm>`_)
|
|
@@ -391,7 +724,8 @@ class Salesforce(object):
|
|
|
391
724
|
:param payload: The JSON payload with the record details
|
|
392
725
|
:type payload: dict
|
|
393
726
|
:returns: The API response from the POST request
|
|
394
|
-
:raises: :py:exc:`RuntimeError`,
|
|
727
|
+
:raises: :py:exc:`RuntimeError`,
|
|
728
|
+
:py:exc:`TypeError`
|
|
395
729
|
"""
|
|
396
730
|
# Ensure the payload is in the appropriate format
|
|
397
731
|
if not isinstance(payload, dict):
|
|
@@ -412,7 +746,8 @@ class Salesforce(object):
|
|
|
412
746
|
:param payload: The JSON payload with the record details to be updated
|
|
413
747
|
:type payload: dict
|
|
414
748
|
:returns: The API response from the PATCH request
|
|
415
|
-
:raises: :py:exc:`RuntimeError`,
|
|
749
|
+
:raises: :py:exc:`RuntimeError`,
|
|
750
|
+
:py:exc:`TypeError`
|
|
416
751
|
"""
|
|
417
752
|
# Ensure the payload is in the appropriate format
|
|
418
753
|
if not isinstance(payload, dict):
|
|
@@ -596,7 +931,8 @@ class Salesforce(object):
|
|
|
596
931
|
:param return_uri: Determines if the URI of the article should be returned rather than the ID (``False`` by default)
|
|
597
932
|
:type return_uri: bool
|
|
598
933
|
:returns: The Article ID or Article URI, or a blank string if no article is found
|
|
599
|
-
:raises: :py:exc:`ValueError
|
|
934
|
+
:raises: :py:exc:`ValueError`,
|
|
935
|
+
:py:exc:`RuntimeError`
|
|
600
936
|
"""
|
|
601
937
|
return knowledge_module.get_article_id_from_number(self.sfdc_object, article_number=article_number,
|
|
602
938
|
sobject=sobject, return_uri=return_uri)
|
|
@@ -616,6 +952,7 @@ class Salesforce(object):
|
|
|
616
952
|
:param page_num: The starting page number (``1`` by default)
|
|
617
953
|
:type page_num: int
|
|
618
954
|
:returns: The list of retrieved knowledge articles
|
|
955
|
+
:raises: :py:exc:`RuntimeError`
|
|
619
956
|
"""
|
|
620
957
|
return knowledge_module.get_articles_list(self.sfdc_object, query=query, sort=sort, order=order,
|
|
621
958
|
page_size=page_size, page_num=page_num)
|
|
@@ -629,6 +966,7 @@ class Salesforce(object):
|
|
|
629
966
|
:param sobject: The Salesforce object to query (``Knowledge__kav`` by default)
|
|
630
967
|
:type sobject: str, None
|
|
631
968
|
:returns: The details for the knowledge article
|
|
969
|
+
:raises: :py:exc:`RuntimeError`
|
|
632
970
|
"""
|
|
633
971
|
return knowledge_module.get_article_details(self.sfdc_object, article_id=article_id, sobject=sobject)
|
|
634
972
|
|
|
@@ -680,7 +1018,8 @@ class Salesforce(object):
|
|
|
680
1018
|
:param sobject: The Salesforce object to query (``Knowledge__kav`` by default)
|
|
681
1019
|
:type sobject: str, None
|
|
682
1020
|
:returns: The article URL as a string
|
|
683
|
-
:raises: :py:exc:`ValueError
|
|
1021
|
+
:raises: :py:exc:`ValueError`,
|
|
1022
|
+
:py:exc:`RuntimeError`
|
|
684
1023
|
"""
|
|
685
1024
|
return knowledge_module.get_article_url(self.sfdc_object, article_id=article_id,
|
|
686
1025
|
article_number=article_number, sobject=sobject)
|
|
@@ -696,7 +1035,9 @@ class Salesforce(object):
|
|
|
696
1035
|
:param full_response: Determines if the full API response should be returned instead of the article ID (``False`` by default)
|
|
697
1036
|
:type full_response: bool
|
|
698
1037
|
:returns: The API response or the ID of the article draft
|
|
699
|
-
:raises: :py:exc:`ValueError`,
|
|
1038
|
+
:raises: :py:exc:`ValueError`,
|
|
1039
|
+
:py:exc:`TypeError`,
|
|
1040
|
+
:py:exc:`RuntimeError`
|
|
700
1041
|
"""
|
|
701
1042
|
return knowledge_module.create_article(self.sfdc_object, article_data=article_data, sobject=sobject,
|
|
702
1043
|
full_response=full_response)
|
|
@@ -714,7 +1055,9 @@ class Salesforce(object):
|
|
|
714
1055
|
:param include_status_code: Determines if the API response status code should be returned (``False`` by default)
|
|
715
1056
|
:type include_status_code: bool
|
|
716
1057
|
:returns: A Boolean indicating if the update operation was successful, and optionally the API response status code
|
|
717
|
-
:raises: :py:exc:`ValueError`,
|
|
1058
|
+
:raises: :py:exc:`ValueError`,
|
|
1059
|
+
:py:exc:`TypeError`,
|
|
1060
|
+
:py:exc:`RuntimeError`
|
|
718
1061
|
"""
|
|
719
1062
|
return knowledge_module.update_article(self.sfdc_object, record_id=record_id, article_data=article_data,
|
|
720
1063
|
sobject=sobject, include_status_code=include_status_code)
|
|
@@ -781,7 +1124,9 @@ class Salesforce(object):
|
|
|
781
1124
|
:param major_version: Determines if the published article should be a major version (``True`` by default)
|
|
782
1125
|
:type major_version: bool
|
|
783
1126
|
:returns: The API response from the POST request
|
|
784
|
-
:raises: :py:exc:`RuntimeError`,
|
|
1127
|
+
:raises: :py:exc:`RuntimeError`,
|
|
1128
|
+
:py:exc:`TypeError`,
|
|
1129
|
+
:py:exc:`ValueError`
|
|
785
1130
|
"""
|
|
786
1131
|
return knowledge_module.publish_multiple_articles(self.sfdc_object, article_id_list=article_id_list,
|
|
787
1132
|
major_version=major_version)
|
|
@@ -790,7 +1135,7 @@ class Salesforce(object):
|
|
|
790
1135
|
"""This method assigns a single data category for a knowledge article.
|
|
791
1136
|
(`Reference <https://itsmemohit.medium.com/quick-win-15-salesforce-knowledge-rest-apis-bb0725b2040e>`_)
|
|
792
1137
|
|
|
793
|
-
..
|
|
1138
|
+
.. version-added:: 1.2.0
|
|
794
1139
|
|
|
795
1140
|
:param article_id: The ID of the article to update
|
|
796
1141
|
:type article_id: str
|
|
@@ -809,7 +1154,7 @@ class Salesforce(object):
|
|
|
809
1154
|
"""This function archives a published knowledge article.
|
|
810
1155
|
(`Reference <https://developer.salesforce.com/docs/atlas.en-us.knowledge_dev.meta/knowledge_dev/knowledge_REST_archive_master_version.htm>`_)
|
|
811
1156
|
|
|
812
|
-
..
|
|
1157
|
+
.. version-added:: 1.3.0
|
|
813
1158
|
|
|
814
1159
|
:param article_id: The ID of the article to archive
|
|
815
1160
|
:type article_id: str
|
|
@@ -818,6 +1163,23 @@ class Salesforce(object):
|
|
|
818
1163
|
"""
|
|
819
1164
|
return knowledge_module.archive_article(self.sfdc_object, article_id=article_id)
|
|
820
1165
|
|
|
1166
|
+
def delete_article_draft(self, version_id: str, use_knowledge_management_endpoint: bool = True):
|
|
1167
|
+
"""This function deletes an unpublished knowledge article draft.
|
|
1168
|
+
|
|
1169
|
+
.. version-added:: 1.4.0
|
|
1170
|
+
|
|
1171
|
+
:param version_id: The 15-character or 18-character ``Id`` (Knowledge Article Version ID) value
|
|
1172
|
+
:type version_id: str
|
|
1173
|
+
:param use_knowledge_management_endpoint: Leverage the ``/knowledgeManagement/articleVersions/masterVersions/``
|
|
1174
|
+
endpoint rather than the ``/sobjects/Knowledge__kav/`` endpoint
|
|
1175
|
+
(``True`` by default)
|
|
1176
|
+
:type use_knowledge_management_endpoint: bool
|
|
1177
|
+
:returns: The API response from the DELETE request
|
|
1178
|
+
:raises: :py:exc:`RuntimeError`
|
|
1179
|
+
"""
|
|
1180
|
+
return knowledge_module.delete_article_draft(self.sfdc_object, version_id=version_id,
|
|
1181
|
+
use_knowledge_management_endpoint=use_knowledge_management_endpoint)
|
|
1182
|
+
|
|
821
1183
|
|
|
822
1184
|
def define_connection_info():
|
|
823
1185
|
"""This function prompts the user for the connection information.
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
:Module: salespyforce.decorators
|
|
4
|
+
:Synopsis: Decorators that can be used to include additional functionality with functions and methods
|
|
5
|
+
:Created By: Jeff Shurtliff
|
|
6
|
+
:Last Modified: Jeff Shurtliff
|
|
7
|
+
:Modified Date: 02 Feb 2026
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import functools
|
|
13
|
+
import warnings
|
|
14
|
+
from typing import Any, Callable, Optional, Type, TypeVar
|
|
15
|
+
|
|
16
|
+
# Define the function Type bound to Callable
|
|
17
|
+
F = TypeVar("F", bound=Callable[..., Any])
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def deprecated(
|
|
21
|
+
*,
|
|
22
|
+
since: str,
|
|
23
|
+
replacement: Optional[str] = None,
|
|
24
|
+
removal: Optional[str] = None,
|
|
25
|
+
category: Type[Warning] = DeprecationWarning,
|
|
26
|
+
stacklevel: int = 2,
|
|
27
|
+
) -> Callable[[F], F]:
|
|
28
|
+
"""This decorator marks a callable as deprecated and emits a warning at runtime.
|
|
29
|
+
|
|
30
|
+
.. version-added:: 1.4.0
|
|
31
|
+
|
|
32
|
+
:param since: Version when deprecation started
|
|
33
|
+
:param replacement: Suggested replacement usage (string)
|
|
34
|
+
:param removal: Version when it will be removed (optional)
|
|
35
|
+
:param category: Warning category (default: DeprecationWarning)
|
|
36
|
+
:param stacklevel: Warning stacklevel (default: 2)
|
|
37
|
+
"""
|
|
38
|
+
def decorator(func: F) -> F:
|
|
39
|
+
message_parts = [f"{func.__name__} is deprecated since {since}."]
|
|
40
|
+
if replacement:
|
|
41
|
+
message_parts.append(f"Use {replacement} instead.")
|
|
42
|
+
if removal:
|
|
43
|
+
message_parts.append(f"It will be removed in {removal}.")
|
|
44
|
+
message = " ".join(message_parts)
|
|
45
|
+
|
|
46
|
+
@functools.wraps(func)
|
|
47
|
+
def wrapper(*args: Any, **kwargs: Any):
|
|
48
|
+
warnings.warn(message, category=category, stacklevel=stacklevel)
|
|
49
|
+
return func(*args, **kwargs)
|
|
50
|
+
|
|
51
|
+
return wrapper # type: ignore[return-value]
|
|
52
|
+
|
|
53
|
+
return decorator
|
salespyforce/errors/handlers.py
CHANGED
|
@@ -4,12 +4,50 @@
|
|
|
4
4
|
:Synopsis: Functions that handle various error situations within the namespace
|
|
5
5
|
:Created By: Jeff Shurtliff
|
|
6
6
|
:Last Modified: Jeff Shurtliff
|
|
7
|
-
:Modified Date:
|
|
7
|
+
:Modified Date: 02 Feb 2026
|
|
8
8
|
"""
|
|
9
9
|
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
10
12
|
import sys
|
|
13
|
+
from typing import Final
|
|
14
|
+
import warnings
|
|
15
|
+
|
|
16
|
+
# Define constants
|
|
17
|
+
_DEFAULT_CATEGORY: Final[type[Warning]] = UserWarning
|
|
11
18
|
|
|
12
19
|
|
|
13
|
-
def eprint(*args, **kwargs):
|
|
20
|
+
def eprint(*args, **kwargs) -> None:
|
|
14
21
|
"""This function behaves the same as the ``print()`` function but is leveraged to print errors to ``sys.stderr``."""
|
|
15
22
|
print(*args, file=sys.stderr, **kwargs)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def get_exception_type(exc) -> str:
|
|
26
|
+
"""This function returns the exception type (e.g. ``RuntimeError``, ``TypeError``, etc.) for a given exception.
|
|
27
|
+
|
|
28
|
+
.. version-added:: 1.4.0
|
|
29
|
+
|
|
30
|
+
:returns: The exception type as a string
|
|
31
|
+
"""
|
|
32
|
+
return type(exc).__name__
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def display_warning(
|
|
36
|
+
message: str,
|
|
37
|
+
*,
|
|
38
|
+
category: type[Warning] = _DEFAULT_CATEGORY,
|
|
39
|
+
stacklevel: int = 2,
|
|
40
|
+
) -> None:
|
|
41
|
+
"""This function emits a warning that points to the caller by default.
|
|
42
|
+
|
|
43
|
+
.. version-added:: 1.4.0
|
|
44
|
+
|
|
45
|
+
:param message: Warning message to emit
|
|
46
|
+
:type message: str
|
|
47
|
+
:param category: Warning category class (default: ``UserWarning``)
|
|
48
|
+
:type category: type[Warning]
|
|
49
|
+
:param stacklevel: How far up the call stack to attribute the warning (``2`` by default - caller of this helper)
|
|
50
|
+
:type stacklevel: int
|
|
51
|
+
:returns: None
|
|
52
|
+
"""
|
|
53
|
+
warnings.warn(message, category=category, stacklevel=stacklevel)
|
salespyforce/knowledge.py
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
:Synopsis: Defines the Knowledge-related functions associated with the Salesforce API
|
|
5
5
|
:Created By: Jeff Shurtliff
|
|
6
6
|
:Last Modified: Jeff Shurtliff
|
|
7
|
-
:Modified Date:
|
|
7
|
+
:Modified Date: 03 Feb 2026
|
|
8
8
|
"""
|
|
9
9
|
|
|
10
10
|
from . import errors
|
|
@@ -20,7 +20,7 @@ def check_for_existing_article(sfdc_object, title, sobject=None, return_id=False
|
|
|
20
20
|
(`Reference 1 <https://developer.salesforce.com/docs/atlas.en-us.api_rest.meta/api_rest/dome_query.htm>`_,
|
|
21
21
|
`Reference 2 <https://developer.salesforce.com/docs/atlas.en-us.knowledge_dev.meta/knowledge_dev/knowledge_development_soql_sosl_intro.htm>`_)
|
|
22
22
|
|
|
23
|
-
..
|
|
23
|
+
.. version-changed:: 1.2.2
|
|
24
24
|
You can now specify whether archived articles are included in the query results.
|
|
25
25
|
|
|
26
26
|
:param sfdc_object: The instantiated SalesPyForce object
|
|
@@ -230,7 +230,7 @@ def get_article_version(sfdc_object, article_id):
|
|
|
230
230
|
def get_article_url(sfdc_object, article_id=None, article_number=None, sobject=None):
|
|
231
231
|
"""This function constructs the URL to view a knowledge article in Lightning or Classic.
|
|
232
232
|
|
|
233
|
-
..
|
|
233
|
+
.. version-changed:: 1.2.0
|
|
234
234
|
Changed when lightning URLs are defined and fixed an issue with extraneous slashes.
|
|
235
235
|
|
|
236
236
|
:param sfdc_object: The instantiated SalesPyForce object
|
|
@@ -483,7 +483,7 @@ def assign_data_category(sfdc_object, article_id, category_group_name, category_
|
|
|
483
483
|
"""This function assigns a single data category for a knowledge article.
|
|
484
484
|
(`Reference <https://itsmemohit.medium.com/quick-win-15-salesforce-knowledge-rest-apis-bb0725b2040e>`_)
|
|
485
485
|
|
|
486
|
-
..
|
|
486
|
+
.. version-added:: 1.2.0
|
|
487
487
|
|
|
488
488
|
:param sfdc_object: The instantiated SalesPyForce object
|
|
489
489
|
:type sfdc_object: class[salespyforce.Salesforce]
|
|
@@ -512,7 +512,7 @@ def archive_article(sfdc_object, article_id):
|
|
|
512
512
|
"""This function archives a published knowledge article.
|
|
513
513
|
(`Reference <https://developer.salesforce.com/docs/atlas.en-us.knowledge_dev.meta/knowledge_dev/knowledge_REST_archive_master_version.htm>`_)
|
|
514
514
|
|
|
515
|
-
..
|
|
515
|
+
.. version-added:: 1.3.0
|
|
516
516
|
|
|
517
517
|
:param sfdc_object: The instantiated SalesPyForce object
|
|
518
518
|
:type sfdc_object: class[salespyforce.Salesforce]
|
|
@@ -529,3 +529,26 @@ def archive_article(sfdc_object, article_id):
|
|
|
529
529
|
# Perform the API call
|
|
530
530
|
endpoint = f'/services/data/{sfdc_object.version}/knowledgeManagement/articleVersions/masterVersions/{article_id}'
|
|
531
531
|
return sfdc_object.patch(endpoint, payload)
|
|
532
|
+
|
|
533
|
+
|
|
534
|
+
def delete_article_draft(sfdc_object, version_id: str, use_knowledge_management_endpoint: bool = True):
|
|
535
|
+
"""This function deletes an unpublished knowledge article draft.
|
|
536
|
+
|
|
537
|
+
.. version-added:: 1.4.0
|
|
538
|
+
|
|
539
|
+
:param sfdc_object: The instantiated SalesPyForce object
|
|
540
|
+
:type sfdc_object: class[salespyforce.Salesforce]
|
|
541
|
+
:param version_id: The 15-character or 18-character ``Id`` (Knowledge Article Version ID) value
|
|
542
|
+
:type version_id: str
|
|
543
|
+
:param use_knowledge_management_endpoint: Leverage the ``/knowledgeManagement/articleVersions/masterVersions/``
|
|
544
|
+
endpoint rather than the ``/sobjects/Knowledge__kav/`` endpoint
|
|
545
|
+
(``True`` by default)
|
|
546
|
+
:type use_knowledge_management_endpoint: bool
|
|
547
|
+
:returns: The API response from the DELETE request
|
|
548
|
+
:raises: :py:exc:`RuntimeError`
|
|
549
|
+
"""
|
|
550
|
+
if use_knowledge_management_endpoint:
|
|
551
|
+
endpoint = f'/services/data/{sfdc_object.version}/knowledgeManagement/articleVersions/masterVersions/{version_id}'
|
|
552
|
+
else:
|
|
553
|
+
endpoint = f'/services/data/{sfdc_object.version}/sobjects/Knowledge__kav/{version_id}'
|
|
554
|
+
return sfdc_object.delete(endpoint)
|
salespyforce/utils/core_utils.py
CHANGED
|
@@ -6,9 +6,10 @@
|
|
|
6
6
|
:Example: ``encoded_string = core_utils.encode_url(decoded_string)``
|
|
7
7
|
:Created By: Jeff Shurtliff
|
|
8
8
|
:Last Modified: Jeff Shurtliff
|
|
9
|
-
:Modified Date:
|
|
9
|
+
:Modified Date: 02 Feb 2026
|
|
10
10
|
"""
|
|
11
11
|
|
|
12
|
+
import re
|
|
12
13
|
import random
|
|
13
14
|
import string
|
|
14
15
|
import os.path
|
|
@@ -19,10 +20,15 @@ import requests
|
|
|
19
20
|
|
|
20
21
|
from . import log_utils
|
|
21
22
|
from .. import errors
|
|
23
|
+
from ..decorators import deprecated
|
|
22
24
|
|
|
23
25
|
# Initialize the logger for this module
|
|
24
26
|
logger = log_utils.initialize_logging(__name__)
|
|
25
27
|
|
|
28
|
+
# Define constants
|
|
29
|
+
SALESFORCE_ID_SUFFIX_ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ012345'
|
|
30
|
+
VALID_SALESFORCE_URL_PATTERN = r'^https://[a-zA-Z0-9._-]+\.salesforce\.com(/|$)'
|
|
31
|
+
|
|
26
32
|
|
|
27
33
|
def url_encode(raw_string):
|
|
28
34
|
"""This function encodes a string for use in URLs.
|
|
@@ -44,9 +50,13 @@ def url_decode(encoded_string):
|
|
|
44
50
|
return urllib.parse.unquote_plus(encoded_string)
|
|
45
51
|
|
|
46
52
|
|
|
53
|
+
@deprecated(since='1.4.0', replacement='salespyforce.errors.handlers.display_warning', removal='2.0.0')
|
|
47
54
|
def display_warning(warn_msg):
|
|
48
55
|
"""This function displays a :py:exc:`UserWarning` message via the :py:mod:`warnings` module.
|
|
49
56
|
|
|
57
|
+
.. deprecated:: 1.4.0
|
|
58
|
+
Use :py:func:`salespyforce.errors.handlers.display_warning` instead.
|
|
59
|
+
|
|
50
60
|
:param warn_msg: The message to be displayed
|
|
51
61
|
:type warn_msg: str
|
|
52
62
|
:returns: None
|
|
@@ -60,7 +70,8 @@ def get_file_type(file_path):
|
|
|
60
70
|
:param file_path: The full path to the file
|
|
61
71
|
:type file_path: str
|
|
62
72
|
:returns: The file type in string format (e.g. ``yaml`` or ``json``)
|
|
63
|
-
:raises: :py:exc:`FileNotFoundError`,
|
|
73
|
+
:raises: :py:exc:`FileNotFoundError`,
|
|
74
|
+
:py:exc:`salespyforce.errors.exceptions.UnknownFileTypeError`
|
|
64
75
|
"""
|
|
65
76
|
file_type = 'unknown'
|
|
66
77
|
if os.path.isfile(file_path):
|
|
@@ -97,6 +108,79 @@ def get_random_string(length=32, prefix_string=""):
|
|
|
97
108
|
return f"{prefix_string}{''.join([random.choice(string.ascii_letters + string.digits) for _ in range(length)])}"
|
|
98
109
|
|
|
99
110
|
|
|
111
|
+
def get_18_char_id(record_id: str) -> str:
|
|
112
|
+
"""This function converts a 15-character Salesforce record ID to its 18-character case-insensitive form.
|
|
113
|
+
|
|
114
|
+
.. version-added:: 1.4.0
|
|
115
|
+
|
|
116
|
+
:param record_id: The Salesforce record ID to convert (or return unchanged if already 18 characters)
|
|
117
|
+
:type record_id: str
|
|
118
|
+
:returns: The 18-character Salesforce record ID
|
|
119
|
+
:raises: :py:exc:`ValueError`
|
|
120
|
+
"""
|
|
121
|
+
# Ensure the provided record ID is a string
|
|
122
|
+
if not isinstance(record_id, str):
|
|
123
|
+
raise ValueError("Salesforce ID must be a string")
|
|
124
|
+
|
|
125
|
+
# Return the record ID unchanged if it is already 18 characters in length
|
|
126
|
+
if len(record_id) == 18:
|
|
127
|
+
return record_id
|
|
128
|
+
|
|
129
|
+
# Ensure the record ID is a valid 15-character value
|
|
130
|
+
if len(record_id) != 15:
|
|
131
|
+
raise ValueError("Salesforce ID must be 15 or 18 characters long")
|
|
132
|
+
|
|
133
|
+
# Define the checksum suffix (additional 3 characters)
|
|
134
|
+
suffix = ""
|
|
135
|
+
for i in range(0, 15, 5):
|
|
136
|
+
chunk = record_id[i:i + 5]
|
|
137
|
+
bitmask = 0
|
|
138
|
+
|
|
139
|
+
for index, char in enumerate(chunk):
|
|
140
|
+
if "A" <= char <= "Z":
|
|
141
|
+
bitmask |= 1 << index
|
|
142
|
+
|
|
143
|
+
suffix += SALESFORCE_ID_SUFFIX_ALPHABET[bitmask]
|
|
144
|
+
|
|
145
|
+
# Return the 18-character ID value
|
|
146
|
+
return record_id + suffix
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def matches_regex_pattern(pattern: str, text: str, full_match: bool = False, must_start_with: bool = False) -> bool:
|
|
150
|
+
"""This function compares a text string against a regex pattern and determines whether they match.
|
|
151
|
+
|
|
152
|
+
.. version-added:: 1.4.0
|
|
153
|
+
|
|
154
|
+
:param pattern: The regex pattern that should match
|
|
155
|
+
:type pattern: str
|
|
156
|
+
:param text: The text string to evaluate
|
|
157
|
+
:type text: str
|
|
158
|
+
:param full_match: Determines if the entire string should be validated
|
|
159
|
+
:type full_match: bool
|
|
160
|
+
:param must_start_with: Determines if the pattern must be at the beginning of the string
|
|
161
|
+
:returns: True if the regex pattern matches anywhere in the text string
|
|
162
|
+
:raises: :py:exc:`TypeError`
|
|
163
|
+
"""
|
|
164
|
+
if full_match:
|
|
165
|
+
return bool(re.fullmatch(pattern, text))
|
|
166
|
+
elif must_start_with:
|
|
167
|
+
return bool(re.match(pattern, text))
|
|
168
|
+
else:
|
|
169
|
+
return bool(re.search(pattern, text))
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def is_valid_salesforce_url(url: str) -> bool:
|
|
173
|
+
"""This function evaluates a URL to determine if it is a valid Salesforce URL.
|
|
174
|
+
|
|
175
|
+
.. version-added:: 1.4.0
|
|
176
|
+
|
|
177
|
+
:param url: The URL to evaluate
|
|
178
|
+
:type url: str
|
|
179
|
+
:returns: Boolean value depending on whether the URL meets the criteria
|
|
180
|
+
"""
|
|
181
|
+
return True if isinstance(url, str) and matches_regex_pattern(VALID_SALESFORCE_URL_PATTERN, url) else False
|
|
182
|
+
|
|
183
|
+
|
|
100
184
|
def get_image_ref_id(image_url):
|
|
101
185
|
"""This function parses an image URL to identify the reference ID (refid) value.
|
|
102
186
|
(`Reference 1 <https://developer.salesforce.com/docs/atlas.en-us.api_rest.meta/api_rest/resources_sobject_rich_text_image_retrieve.htm>`_,
|
salespyforce/utils/log_utils.py
CHANGED
|
@@ -121,7 +121,7 @@ def _set_logging_level(_logger, _log_level):
|
|
|
121
121
|
"""This function sets the logging level for a :py:class:`logging.Logger` instance.
|
|
122
122
|
|
|
123
123
|
:param _logger: The :py:class:`logging.Logger` instance
|
|
124
|
-
:type _logger: Logger
|
|
124
|
+
:type _logger: class[logging.Logger]
|
|
125
125
|
:param _log_level: The log level as a string (``debug``, ``info``, ``warning``, ``error`` or ``critical``)
|
|
126
126
|
:type _log_level: str
|
|
127
127
|
:returns: The :py:class:`logging.Logger` instance with a logging level set where applicable
|
|
@@ -162,7 +162,7 @@ def _add_file_handler(_logger, _log_level, _log_file, _overwrite, _formatter):
|
|
|
162
162
|
"""This function adds a :py:class:`logging.FileHandler` to the :py:class:`logging.Logger` instance.
|
|
163
163
|
|
|
164
164
|
:param _logger: The :py:class:`logging.Logger` instance
|
|
165
|
-
:type _logger: Logger
|
|
165
|
+
:type _logger: class[logging.Logger]
|
|
166
166
|
:param _log_level: The log level to set for the handler
|
|
167
167
|
:type _log_level: str
|
|
168
168
|
:param _log_file: The log file (as a file name or a file path) to which messages should be written
|
|
@@ -174,7 +174,7 @@ def _add_file_handler(_logger, _log_level, _log_file, _overwrite, _formatter):
|
|
|
174
174
|
:param _overwrite: Determines if messages should be appended to the file (default) or overwrite it
|
|
175
175
|
:type _overwrite: bool
|
|
176
176
|
:param _formatter: The :py:class:`logging.Formatter` to apply to messages passed through the handler
|
|
177
|
-
:type _formatter: Formatter
|
|
177
|
+
:type _formatter: class[logging.Formatter]
|
|
178
178
|
:returns: The :py:class:`logging.Logger` instance with the added :py:class:`logging.FileHandler`
|
|
179
179
|
"""
|
|
180
180
|
# Define the log file to use
|
|
@@ -203,11 +203,11 @@ def _add_stream_handler(_logger, _log_level, _formatter):
|
|
|
203
203
|
"""This function adds a :py:class:`logging.StreamHandler` to the :py:class:`logging.Logger` instance.
|
|
204
204
|
|
|
205
205
|
:param _logger: The :py:class:`logging.Logger` instance
|
|
206
|
-
:type _logger: Logger
|
|
206
|
+
:type _logger: class[logging.Logger]
|
|
207
207
|
:param _log_level: The log level to set for the handler
|
|
208
208
|
:type _log_level: str
|
|
209
209
|
:param _formatter: The :py:class:`logging.Formatter` to apply to messages passed through the handler
|
|
210
|
-
:type _formatter: Formatter
|
|
210
|
+
:type _formatter: class[logging.Formatter]
|
|
211
211
|
:returns: The :py:class:`logging.Logger` instance with the added :py:class:`logging.StreamHandler`
|
|
212
212
|
"""
|
|
213
213
|
_log_level = HANDLER_DEFAULTS.get('console_log_level') if not _log_level else _log_level
|
|
@@ -229,11 +229,11 @@ def _add_split_stream_handlers(_logger, _log_level, _formatter):
|
|
|
229
229
|
more information on how this filtering is implemented and for credit to the original author.
|
|
230
230
|
|
|
231
231
|
:param _logger: The :py:class:`logging.Logger` instance
|
|
232
|
-
:type _logger: Logger
|
|
232
|
+
:type _logger: class[logging.Logger]
|
|
233
233
|
:param _log_level: The log level provided for the stream handler (i.e. console output)
|
|
234
234
|
:type _log_level: str
|
|
235
235
|
:param _formatter: The :py:class:`logging.Formatter` to apply to messages passed through the handlers
|
|
236
|
-
:type _formatter: Formatter
|
|
236
|
+
:type _formatter: class[logging.Formatter]
|
|
237
237
|
:returns: The logger instance with the two handlers added
|
|
238
238
|
"""
|
|
239
239
|
# Configure and add the STDOUT handler
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
:Synopsis: This module is used by pytest to test core utility functions
|
|
6
6
|
:Created By: Jeff Shurtliff
|
|
7
7
|
:Last Modified: Jeff Shurtliff
|
|
8
|
-
:Modified Date:
|
|
8
|
+
:Modified Date: 30 Jan 2026
|
|
9
9
|
"""
|
|
10
10
|
|
|
11
11
|
import os
|
|
@@ -115,6 +115,45 @@ def test_get_random_string_returns_expected_length(monkeypatch):
|
|
|
115
115
|
assert all(char in alphabet for char in result.replace("pre_", ""))
|
|
116
116
|
|
|
117
117
|
|
|
118
|
+
def test_converts_15_char_id_to_18_char():
|
|
119
|
+
"""This function tests the conversion of a 15-character Salesforce ID into the 18-character equivalent.
|
|
120
|
+
|
|
121
|
+
.. version-added:: 1.4.0
|
|
122
|
+
"""
|
|
123
|
+
id_15 = "ka4PO0000002hby"
|
|
124
|
+
id_18 = core_utils.get_18_char_id(id_15)
|
|
125
|
+
|
|
126
|
+
assert len(id_18) == 18
|
|
127
|
+
assert id_18.startswith(id_15)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def test_returns_18_char_id_unchanged():
|
|
131
|
+
"""This function tests that an 18-character Salesforce ID is returned unchanged during conversion attempt.
|
|
132
|
+
|
|
133
|
+
.. version-added:: 1.4.0
|
|
134
|
+
"""
|
|
135
|
+
id_18 = "ka4PO0000002hbyYAA"
|
|
136
|
+
assert core_utils.get_18_char_id(id_18) == id_18
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def test_invalid_id_length_raises_error():
|
|
140
|
+
"""This function tests to ensure passing an invalid Salesforce ID length raises an exception.
|
|
141
|
+
|
|
142
|
+
.. version-added:: 1.4.0
|
|
143
|
+
"""
|
|
144
|
+
with pytest.raises(ValueError):
|
|
145
|
+
core_utils.get_18_char_id("short")
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def test_non_string_id_input_raises_error():
|
|
149
|
+
"""This function tests to ensure passing a non-string to the get_18_char_id function raises an exception.
|
|
150
|
+
|
|
151
|
+
.. version-added:: 1.4.0
|
|
152
|
+
"""
|
|
153
|
+
with pytest.raises(ValueError):
|
|
154
|
+
core_utils.get_18_char_id(12345)
|
|
155
|
+
|
|
156
|
+
|
|
118
157
|
def test_get_image_ref_id_parses_query_param():
|
|
119
158
|
"""This function tests get_image_ref_id parsing.
|
|
120
159
|
|
|
@@ -22,7 +22,7 @@ def _cleanup_logger(logger: logging.Logger) -> None:
|
|
|
22
22
|
.. version-added:: 1.4.0
|
|
23
23
|
|
|
24
24
|
:param logger: The logger instance to clean up
|
|
25
|
-
:type logger: logging.Logger
|
|
25
|
+
:type logger: class[logging.Logger]
|
|
26
26
|
:returns: None
|
|
27
27
|
"""
|
|
28
28
|
for handler in list(logger.handlers):
|
|
@@ -51,7 +51,7 @@ def test_initialize_logging_applies_default_level_to_console_handler(caplog: pyt
|
|
|
51
51
|
.. version-added:: 1.4.0
|
|
52
52
|
|
|
53
53
|
:param caplog: Pytest fixture capturing log records for assertions
|
|
54
|
-
:type caplog: pytest.LogCaptureFixture
|
|
54
|
+
:type caplog: class[pytest.LogCaptureFixture]
|
|
55
55
|
:returns: None
|
|
56
56
|
"""
|
|
57
57
|
logger_name = "salespyforce.test.console.default"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: salespyforce
|
|
3
|
-
Version: 1.4.
|
|
3
|
+
Version: 1.4.0rc0
|
|
4
4
|
Summary: A Python toolset for performing Salesforce API calls
|
|
5
5
|
License: MIT License
|
|
6
6
|
|
|
@@ -63,7 +63,7 @@ A Python toolset for performing Salesforce API calls
|
|
|
63
63
|
<td>Latest Beta/RC Release</td>
|
|
64
64
|
<td>
|
|
65
65
|
<a href='https://pypi.org/project/salespyforce/#history'>
|
|
66
|
-
<img alt="PyPI" src="https://img.shields.io/badge/pypi-1.4.0.
|
|
66
|
+
<img alt="PyPI" src="https://img.shields.io/badge/pypi-1.4.0.dev2-blue">
|
|
67
67
|
</a>
|
|
68
68
|
</td>
|
|
69
69
|
</tr>
|
|
@@ -151,14 +151,6 @@ The package can be installed via pip using the syntax below.
|
|
|
151
151
|
pip install salespyforce --upgrade
|
|
152
152
|
```
|
|
153
153
|
|
|
154
|
-
You may also clone the repository and install from source using below.
|
|
155
|
-
|
|
156
|
-
```sh
|
|
157
|
-
git clone git://github.com/jeffshurtliff/salespyforce.git
|
|
158
|
-
cd salespyforce/
|
|
159
|
-
python setup.py install
|
|
160
|
-
```
|
|
161
|
-
|
|
162
154
|
## Change Log
|
|
163
155
|
The change log can be found in the [documentation](https://salespyforce.readthedocs.io/en/latest/changelog.html).
|
|
164
156
|
|
|
@@ -1,27 +1,28 @@
|
|
|
1
1
|
salespyforce/__init__.py,sha256=W2RY2_kojLcXtTHJrto5FtU6q1umVPaU1RZe_QkFgNY,2031
|
|
2
|
-
salespyforce/api.py,sha256=
|
|
2
|
+
salespyforce/api.py,sha256=NdvPUqJ5HrcaEBaHte414eNOgwIaNweSYxz3E6-zWQE,9403
|
|
3
3
|
salespyforce/chatter.py,sha256=GI9iXmq2mrbrH7daW8Kc3gt2O04dRthSVocJq4BVwCU,7494
|
|
4
|
-
salespyforce/core.py,sha256=
|
|
4
|
+
salespyforce/core.py,sha256=LW0ImVoN67E78FpmqW9BWHQ7JdNKKFM34Qtjv7oP7WI,68099
|
|
5
|
+
salespyforce/decorators.py,sha256=wSUXM6hXaXwLrD9Gzn0JBj_efHWyJ4n74qg1mel_tcQ,1742
|
|
5
6
|
salespyforce/errors/__init__.py,sha256=Tnl4lB2ycpK2UmwmoBoSYuWDtovDNA4CxC5Cku2hDYs,320
|
|
6
7
|
salespyforce/errors/exceptions.py,sha256=6ATDNCPNXnjOzXcaaLZVUy39lsFt2Se5cCvqkd_8faM,17057
|
|
7
|
-
salespyforce/errors/handlers.py,sha256=
|
|
8
|
-
salespyforce/knowledge.py,sha256=
|
|
8
|
+
salespyforce/errors/handlers.py,sha256=Ykap4LBUTKQSbbAb7OMpWGfwvIx_Rl5aDtgAQ1I9ovk,1556
|
|
9
|
+
salespyforce/knowledge.py,sha256=7TQcPWvHTGUhTUsky9tvOPDCsomaVgahg6gxwMjmdP8,25983
|
|
9
10
|
salespyforce/utils/__init__.py,sha256=ATE6BsWd2qDxsy1fQpvaoSJimZgfCmNPYpcFEucES6Q,292
|
|
10
|
-
salespyforce/utils/core_utils.py,sha256=
|
|
11
|
+
salespyforce/utils/core_utils.py,sha256=Q-xtBXVkuK50Ps9lLMZuGfFrSM0JxroN_6jcZkyaK6k,8690
|
|
11
12
|
salespyforce/utils/helper.py,sha256=zwbysqwddWwhp3mfzKJf0aeTTkVqoR35L9dLu6ZVJtU,5661
|
|
12
|
-
salespyforce/utils/log_utils.py,sha256
|
|
13
|
+
salespyforce/utils/log_utils.py,sha256=-7qgSNCUQRstRgz2wXBX6LRWO-GMjIXS5WjxkHGvBHk,11936
|
|
13
14
|
salespyforce/utils/tests/__init__.py,sha256=Iv7M9gPyKv5f5tR-pxc089cd2nrWn5M0ZZpWqiB2htc,242
|
|
14
15
|
salespyforce/utils/tests/conftest.py,sha256=zYB5pYNtRHc6cIxY_cBMQHNM52Is6NjLfcCywCD3-Bw,6661
|
|
15
16
|
salespyforce/utils/tests/resources.py,sha256=-PEqzLMsEQSVj3dz_8VTHXINbSeMvmjX3gZRMTJgoac,4774
|
|
16
|
-
salespyforce/utils/tests/test_core_utils.py,sha256=
|
|
17
|
+
salespyforce/utils/tests/test_core_utils.py,sha256=8b3qjzr028wVeQAxBvIeajcqnWCsqUhNgqcquznGQKg,7272
|
|
17
18
|
salespyforce/utils/tests/test_instantiate_object.py,sha256=O_DTljj_QCBZlDiA1KBUmSL2CZfsVU_7K2h6ypEN0D8,2002
|
|
18
|
-
salespyforce/utils/tests/test_log_utils.py,sha256=
|
|
19
|
+
salespyforce/utils/tests/test_log_utils.py,sha256=FxPC21vXRQbFvqmTi_FL1DsGwuH8JHPkl9XytVoe7uY,2328
|
|
19
20
|
salespyforce/utils/tests/test_sobjects.py,sha256=AeKli_LjSkeaspGxHZoZsM1IafKS2HkJ1wzajSXt6Dc,2120
|
|
20
21
|
salespyforce/utils/tests/test_soql.py,sha256=SSOAN-4QZNpwUON-cLndb5W6AAs28Ja1u-Zt1WBx0VA,776
|
|
21
22
|
salespyforce/utils/tests/test_sosl.py,sha256=3Ny1uF0onjcvI_InfMYRdh31rR5MJADTB3DWSm--EjE,868
|
|
22
23
|
salespyforce/utils/tests/test_version_utils.py,sha256=njRKMiwtCCyUHEAFvfxvavuh03fJ_EupM1TUYmdsZAo,2465
|
|
23
24
|
salespyforce/utils/version.py,sha256=uEWnq3DHmN9wZVohiRCRE1KzNF1XVhP4oVY4vBBgoVU,1781
|
|
24
|
-
salespyforce-1.4.
|
|
25
|
-
salespyforce-1.4.
|
|
26
|
-
salespyforce-1.4.
|
|
27
|
-
salespyforce-1.4.
|
|
25
|
+
salespyforce-1.4.0rc0.dist-info/LICENSE,sha256=b_Ch0pvcZN6Mr4dQ9Bo3It-J3MihQYcj_-EIdaZHuWw,1071
|
|
26
|
+
salespyforce-1.4.0rc0.dist-info/METADATA,sha256=uLUbEpNW2OnNfbWXak8I2oXfeQxggGHD2WS2Xl619fM,10398
|
|
27
|
+
salespyforce-1.4.0rc0.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
|
|
28
|
+
salespyforce-1.4.0rc0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|