salespyforce 1.4.0.dev0__py3-none-any.whl → 1.4.0.dev2__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 +44 -1
- salespyforce/core.py +94 -13
- salespyforce/knowledge.py +21 -5
- salespyforce/utils/core_utils.py +44 -2
- salespyforce/utils/log_utils.py +17 -16
- salespyforce/utils/tests/conftest.py +202 -0
- salespyforce/utils/tests/test_core_utils.py +234 -0
- salespyforce/utils/tests/test_instantiate_object.py +31 -18
- salespyforce/utils/tests/test_log_utils.py +79 -0
- salespyforce/utils/tests/test_sobjects.py +22 -15
- salespyforce/utils/tests/test_soql.py +8 -8
- salespyforce/utils/tests/test_sosl.py +9 -9
- salespyforce/utils/tests/test_version_utils.py +70 -0
- {salespyforce-1.4.0.dev0.dist-info → salespyforce-1.4.0.dev2.dist-info}/METADATA +6 -10
- salespyforce-1.4.0.dev2.dist-info/RECORD +27 -0
- salespyforce-1.4.0.dev0.dist-info/RECORD +0 -23
- {salespyforce-1.4.0.dev0.dist-info → salespyforce-1.4.0.dev2.dist-info}/LICENSE +0 -0
- {salespyforce-1.4.0.dev0.dist-info → salespyforce-1.4.0.dev2.dist-info}/WHEEL +0 -0
salespyforce/api.py
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
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: 29 Jan 2026
|
|
8
8
|
"""
|
|
9
9
|
|
|
10
10
|
import requests
|
|
@@ -117,6 +117,49 @@ def api_call_with_payload(sfdc_object, method, endpoint, payload, params=None, h
|
|
|
117
117
|
return response
|
|
118
118
|
|
|
119
119
|
|
|
120
|
+
def delete(sfdc_object, endpoint, params=None, headers=None, timeout=30, show_full_error=True, return_json=True):
|
|
121
|
+
"""This method performs a DELETE request against the Salesforce instance.
|
|
122
|
+
|
|
123
|
+
.. version-added:: 1.4.0
|
|
124
|
+
|
|
125
|
+
:param sfdc_object: The instantiated SalesPyForce object
|
|
126
|
+
:param endpoint: The API endpoint to query
|
|
127
|
+
:type endpoint: str
|
|
128
|
+
:param params: The query parameters (where applicable)
|
|
129
|
+
:type params: dict, None
|
|
130
|
+
:param headers: Specific API headers to use when performing the API call
|
|
131
|
+
:type headers: dict, None
|
|
132
|
+
:param timeout: The timeout period in seconds (defaults to ``30``)
|
|
133
|
+
:type timeout: int, str, None
|
|
134
|
+
:param show_full_error: Determines if the full error message should be displayed (defaults to ``True``)
|
|
135
|
+
:type show_full_error: bool
|
|
136
|
+
:param return_json: Determines if the response should be returned in JSON format (defaults to ``True``)
|
|
137
|
+
:returns: The API response in JSON format or as a ``requests`` object
|
|
138
|
+
"""
|
|
139
|
+
# Define the parameters as an empty dictionary if none are provided
|
|
140
|
+
params = {} if params is None else params
|
|
141
|
+
|
|
142
|
+
# Define the headers
|
|
143
|
+
default_headers = _get_headers(sfdc_object.access_token)
|
|
144
|
+
headers = default_headers if not headers else headers
|
|
145
|
+
|
|
146
|
+
# Make sure the endpoint begins with a slash
|
|
147
|
+
endpoint = f'/{endpoint}' if not endpoint.startswith('/') else endpoint
|
|
148
|
+
|
|
149
|
+
# Perform the API call
|
|
150
|
+
response = requests.delete(f'{sfdc_object.instance_url}{endpoint}', headers=headers, params=params,
|
|
151
|
+
timeout=timeout)
|
|
152
|
+
if response.status_code >= 300:
|
|
153
|
+
if show_full_error:
|
|
154
|
+
raise RuntimeError(f'The DELETE request failed with a {response.status_code} status code.\n'
|
|
155
|
+
f'{response.text}')
|
|
156
|
+
else:
|
|
157
|
+
raise RuntimeError(f'The DELETE request failed with a {response.status_code} status code.')
|
|
158
|
+
if return_json:
|
|
159
|
+
response = response.json()
|
|
160
|
+
return response
|
|
161
|
+
|
|
162
|
+
|
|
120
163
|
def _get_headers(_access_token, _header_type='default'):
|
|
121
164
|
"""This function returns the appropriate HTTP headers to use for different types of API calls."""
|
|
122
165
|
headers = {
|
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: 30 Jan 2026
|
|
10
10
|
"""
|
|
11
11
|
|
|
12
12
|
import re
|
|
@@ -60,7 +60,8 @@ class Salesforce(object):
|
|
|
60
60
|
:param helper: The file path of a helper file
|
|
61
61
|
:type helper: str, None
|
|
62
62
|
:returns: The instantiated object
|
|
63
|
-
:raises: :py:exc:`TypeError
|
|
63
|
+
:raises: :py:exc:`TypeError`,
|
|
64
|
+
:py:exc:`RuntimeError`
|
|
64
65
|
"""
|
|
65
66
|
# Define the default settings
|
|
66
67
|
self._helper_settings = {}
|
|
@@ -147,6 +148,7 @@ class Salesforce(object):
|
|
|
147
148
|
(`Reference <https://jereze.com/code/authentification-salesforce-rest-api-python/>`_)
|
|
148
149
|
|
|
149
150
|
:returns: The API call response with the authorization information
|
|
151
|
+
:raises: :py:exc:`RuntimeError`
|
|
150
152
|
"""
|
|
151
153
|
params = {
|
|
152
154
|
'grant_type': 'password',
|
|
@@ -176,6 +178,7 @@ class Salesforce(object):
|
|
|
176
178
|
:type show_full_error: bool
|
|
177
179
|
:param return_json: Determines if the response should be returned in JSON format (defaults to ``True``)
|
|
178
180
|
:returns: The API response in JSON format or as a ``requests`` object
|
|
181
|
+
:raises: :py:exc:`RuntimeError`
|
|
179
182
|
"""
|
|
180
183
|
return api.get(self, endpoint=endpoint, params=params, headers=headers, timeout=timeout,
|
|
181
184
|
show_full_error=show_full_error, return_json=return_json)
|
|
@@ -201,6 +204,7 @@ class Salesforce(object):
|
|
|
201
204
|
:type show_full_error: bool
|
|
202
205
|
:param return_json: Determines if the response should be returned in JSON format (defaults to ``True``)
|
|
203
206
|
:returns: The API response in JSON format or as a ``requests`` object
|
|
207
|
+
:raises: :py:exc:`RuntimeError`
|
|
204
208
|
"""
|
|
205
209
|
return api.api_call_with_payload(self, method=method, endpoint=endpoint, payload=payload, params=params,
|
|
206
210
|
headers=headers, timeout=timeout, show_full_error=show_full_error,
|
|
@@ -224,6 +228,7 @@ class Salesforce(object):
|
|
|
224
228
|
:type show_full_error: bool
|
|
225
229
|
:param return_json: Determines if the response should be returned in JSON format (defaults to ``True``)
|
|
226
230
|
:returns: The API response in JSON format or as a ``requests`` object
|
|
231
|
+
:raises: :py:exc:`RuntimeError`
|
|
227
232
|
"""
|
|
228
233
|
return api.api_call_with_payload(self, 'post', endpoint=endpoint, payload=payload, params=params,
|
|
229
234
|
headers=headers, timeout=timeout, show_full_error=show_full_error,
|
|
@@ -247,6 +252,7 @@ class Salesforce(object):
|
|
|
247
252
|
:type show_full_error: bool
|
|
248
253
|
:param return_json: Determines if the response should be returned in JSON format (defaults to ``True``)
|
|
249
254
|
:returns: The API response in JSON format or as a ``requests`` object
|
|
255
|
+
:raises: :py:exc:`RuntimeError`
|
|
250
256
|
"""
|
|
251
257
|
return api.api_call_with_payload(self, 'patch', endpoint=endpoint, payload=payload, params=params,
|
|
252
258
|
headers=headers, timeout=timeout, show_full_error=show_full_error,
|
|
@@ -270,16 +276,41 @@ class Salesforce(object):
|
|
|
270
276
|
:type show_full_error: bool
|
|
271
277
|
:param return_json: Determines if the response should be returned in JSON format (defaults to ``True``)
|
|
272
278
|
:returns: The API response in JSON format or as a ``requests`` object
|
|
279
|
+
:raises: :py:exc:`RuntimeError`
|
|
273
280
|
"""
|
|
274
281
|
return api.api_call_with_payload(self, 'put', endpoint=endpoint, payload=payload, params=params,
|
|
275
282
|
headers=headers, timeout=timeout, show_full_error=show_full_error,
|
|
276
283
|
return_json=return_json)
|
|
277
284
|
|
|
285
|
+
def delete(self, endpoint, params=None, headers=None, timeout=30, show_full_error=True, return_json=True):
|
|
286
|
+
"""This method performs a DELETE request against the Salesforce instance.
|
|
287
|
+
(`Reference <https://jereze.com/code/authentification-salesforce-rest-api-python/>`_)
|
|
288
|
+
|
|
289
|
+
.. version-added:: 1.4.0
|
|
290
|
+
|
|
291
|
+
:param endpoint: The API endpoint to query
|
|
292
|
+
:type endpoint: str
|
|
293
|
+
:param params: The query parameters (where applicable)
|
|
294
|
+
:type params: dict, None
|
|
295
|
+
:param headers: Specific API headers to use when performing the API call
|
|
296
|
+
:type headers: dict, None
|
|
297
|
+
:param timeout: The timeout period in seconds (defaults to ``30``)
|
|
298
|
+
:type timeout: int, str, None
|
|
299
|
+
:param show_full_error: Determines if the full error message should be displayed (defaults to ``True``)
|
|
300
|
+
:type show_full_error: bool
|
|
301
|
+
:param return_json: Determines if the response should be returned in JSON format (defaults to ``True``)
|
|
302
|
+
:returns: The API response in JSON format or as a ``requests`` object
|
|
303
|
+
:raises: :py:exc:`RuntimeError`
|
|
304
|
+
"""
|
|
305
|
+
return api.delete(self, endpoint=endpoint, params=params, headers=headers, timeout=timeout,
|
|
306
|
+
show_full_error=show_full_error, return_json=return_json)
|
|
307
|
+
|
|
278
308
|
def get_api_versions(self) -> list:
|
|
279
309
|
"""This method returns the API versions for the Salesforce releases.
|
|
280
310
|
(`Reference <https://developer.salesforce.com/docs/atlas.en-us.api_rest.meta/api_rest/dome_versions.htm>`_)
|
|
281
311
|
|
|
282
312
|
:returns: A list containing the API metadata from the ``/services/data`` endpoint.
|
|
313
|
+
:raises: :py:exc:`RuntimeError`
|
|
283
314
|
"""
|
|
284
315
|
return self.get('/services/data')
|
|
285
316
|
|
|
@@ -305,13 +336,19 @@ class Salesforce(object):
|
|
|
305
336
|
def get_org_limits(self):
|
|
306
337
|
"""This method returns a list of all org limits.
|
|
307
338
|
|
|
308
|
-
..
|
|
339
|
+
.. version-added:: 1.1.0
|
|
340
|
+
|
|
341
|
+
:returns: The Salesforce org governor limits data
|
|
342
|
+
:raises: :py:exc:`RuntimeError`
|
|
309
343
|
"""
|
|
310
344
|
return self.get(f'/services/data/{self.version}/limits')
|
|
311
345
|
|
|
312
346
|
def get_all_sobjects(self):
|
|
313
347
|
"""This method returns a list of all Salesforce objects. (i.e. sObjects)
|
|
314
348
|
(`Reference <https://developer.salesforce.com/docs/atlas.en-us.api_rest.meta/api_rest/dome_describeGlobal.htm>`_)
|
|
349
|
+
|
|
350
|
+
:returns: The list of all Salesforce objects
|
|
351
|
+
:raises: :py:exc:`RuntimeError`
|
|
315
352
|
"""
|
|
316
353
|
return self.get(f'/services/data/{self.version}/sobjects')
|
|
317
354
|
|
|
@@ -325,6 +362,7 @@ class Salesforce(object):
|
|
|
325
362
|
:param describe: Determines if the full (i.e. ``describe``) data should be returned (defaults to ``False``)
|
|
326
363
|
:type describe: bool
|
|
327
364
|
:returns: The Salesforce object data
|
|
365
|
+
:raises: :py:exc:`RuntimeError`
|
|
328
366
|
"""
|
|
329
367
|
uri = f'/services/data/{self.version}/sobjects/{object_name}'
|
|
330
368
|
uri = f'{uri}/describe' if describe else uri
|
|
@@ -337,15 +375,32 @@ class Salesforce(object):
|
|
|
337
375
|
:param object_name: The name of the Salesforce object
|
|
338
376
|
:type object_name: str
|
|
339
377
|
:returns: The Salesforce object data
|
|
378
|
+
:raises: :py:exc:`RuntimeError`
|
|
340
379
|
"""
|
|
341
380
|
return self.get_sobject(object_name, describe=True)
|
|
342
381
|
|
|
343
382
|
def get_rest_resources(self):
|
|
344
383
|
"""This method returns a list of all available REST resources.
|
|
345
384
|
(`Reference <https://developer.salesforce.com/docs/atlas.en-us.api_rest.meta/api_rest/dome_discoveryresource.htm>`_)
|
|
385
|
+
|
|
386
|
+
:returns: The list of all available REST resources for the Salesforce org
|
|
387
|
+
:raises: :py:exc:`RuntimeError`
|
|
346
388
|
"""
|
|
347
389
|
return self.get(f'/services/data/{self.version}')
|
|
348
390
|
|
|
391
|
+
@staticmethod
|
|
392
|
+
def get_18_char_id(record_id: str) -> str:
|
|
393
|
+
"""This method converts a 15-character Salesforce record ID to its 18-character case-insensitive form.
|
|
394
|
+
|
|
395
|
+
.. version-added:: 1.4.0
|
|
396
|
+
|
|
397
|
+
:param record_id: The Salesforce record ID to convert (or return unchanged if already 18 characters)
|
|
398
|
+
:type record_id: str
|
|
399
|
+
:returns: The 18-character Salesforce record ID
|
|
400
|
+
:raises: :py:exc:`ValueError`
|
|
401
|
+
"""
|
|
402
|
+
return core_utils.get_18_char_id(record_id=record_id)
|
|
403
|
+
|
|
349
404
|
def soql_query(self, query, replace_quotes=True, next_records_url=False):
|
|
350
405
|
"""This method performs a SOQL query and returns the results in JSON format.
|
|
351
406
|
(`Reference 1 <https://developer.salesforce.com/docs/atlas.en-us.api_rest.meta/api_rest/dome_query.htm>`_,
|
|
@@ -358,6 +413,7 @@ class Salesforce(object):
|
|
|
358
413
|
:param next_records_url: Indicates that the ``query`` parameter is a ``nextRecordsUrl`` value.
|
|
359
414
|
:type next_records_url: bool
|
|
360
415
|
:returns: The result of the SOQL query
|
|
416
|
+
:raises: :py:exc:`RuntimeError`
|
|
361
417
|
"""
|
|
362
418
|
if next_records_url:
|
|
363
419
|
query = re.sub(r'^.*/', '', query) if '/' in query else query
|
|
@@ -372,11 +428,12 @@ class Salesforce(object):
|
|
|
372
428
|
"""This method performs a SOSL query to search for a given string.
|
|
373
429
|
(`Reference <https://developer.salesforce.com/docs/atlas.en-us.api_rest.meta/api_rest/resources_search.htm>`_)
|
|
374
430
|
|
|
375
|
-
..
|
|
431
|
+
.. version-added:: 1.1.0
|
|
376
432
|
|
|
377
433
|
:param string_to_search: The string for which to search
|
|
378
434
|
:type string_to_search: str
|
|
379
435
|
:returns: The SOSL response data in JSON format
|
|
436
|
+
:raises: :py:exc:`RuntimeError`
|
|
380
437
|
"""
|
|
381
438
|
query = 'FIND {' + string_to_search + '}'
|
|
382
439
|
query = core_utils.url_encode(query)
|
|
@@ -391,7 +448,8 @@ class Salesforce(object):
|
|
|
391
448
|
:param payload: The JSON payload with the record details
|
|
392
449
|
:type payload: dict
|
|
393
450
|
:returns: The API response from the POST request
|
|
394
|
-
:raises: :py:exc:`RuntimeError`,
|
|
451
|
+
:raises: :py:exc:`RuntimeError`,
|
|
452
|
+
:py:exc:`TypeError`
|
|
395
453
|
"""
|
|
396
454
|
# Ensure the payload is in the appropriate format
|
|
397
455
|
if not isinstance(payload, dict):
|
|
@@ -412,7 +470,8 @@ class Salesforce(object):
|
|
|
412
470
|
:param payload: The JSON payload with the record details to be updated
|
|
413
471
|
:type payload: dict
|
|
414
472
|
:returns: The API response from the PATCH request
|
|
415
|
-
:raises: :py:exc:`RuntimeError`,
|
|
473
|
+
:raises: :py:exc:`RuntimeError`,
|
|
474
|
+
:py:exc:`TypeError`
|
|
416
475
|
"""
|
|
417
476
|
# Ensure the payload is in the appropriate format
|
|
418
477
|
if not isinstance(payload, dict):
|
|
@@ -596,7 +655,8 @@ class Salesforce(object):
|
|
|
596
655
|
:param return_uri: Determines if the URI of the article should be returned rather than the ID (``False`` by default)
|
|
597
656
|
:type return_uri: bool
|
|
598
657
|
:returns: The Article ID or Article URI, or a blank string if no article is found
|
|
599
|
-
:raises: :py:exc:`ValueError
|
|
658
|
+
:raises: :py:exc:`ValueError`,
|
|
659
|
+
:py:exc:`RuntimeError`
|
|
600
660
|
"""
|
|
601
661
|
return knowledge_module.get_article_id_from_number(self.sfdc_object, article_number=article_number,
|
|
602
662
|
sobject=sobject, return_uri=return_uri)
|
|
@@ -616,6 +676,7 @@ class Salesforce(object):
|
|
|
616
676
|
:param page_num: The starting page number (``1`` by default)
|
|
617
677
|
:type page_num: int
|
|
618
678
|
:returns: The list of retrieved knowledge articles
|
|
679
|
+
:raises: :py:exc:`RuntimeError`
|
|
619
680
|
"""
|
|
620
681
|
return knowledge_module.get_articles_list(self.sfdc_object, query=query, sort=sort, order=order,
|
|
621
682
|
page_size=page_size, page_num=page_num)
|
|
@@ -629,6 +690,7 @@ class Salesforce(object):
|
|
|
629
690
|
:param sobject: The Salesforce object to query (``Knowledge__kav`` by default)
|
|
630
691
|
:type sobject: str, None
|
|
631
692
|
:returns: The details for the knowledge article
|
|
693
|
+
:raises: :py:exc:`RuntimeError`
|
|
632
694
|
"""
|
|
633
695
|
return knowledge_module.get_article_details(self.sfdc_object, article_id=article_id, sobject=sobject)
|
|
634
696
|
|
|
@@ -680,7 +742,8 @@ class Salesforce(object):
|
|
|
680
742
|
:param sobject: The Salesforce object to query (``Knowledge__kav`` by default)
|
|
681
743
|
:type sobject: str, None
|
|
682
744
|
:returns: The article URL as a string
|
|
683
|
-
:raises: :py:exc:`ValueError
|
|
745
|
+
:raises: :py:exc:`ValueError`,
|
|
746
|
+
:py:exc:`RuntimeError`
|
|
684
747
|
"""
|
|
685
748
|
return knowledge_module.get_article_url(self.sfdc_object, article_id=article_id,
|
|
686
749
|
article_number=article_number, sobject=sobject)
|
|
@@ -696,7 +759,9 @@ class Salesforce(object):
|
|
|
696
759
|
:param full_response: Determines if the full API response should be returned instead of the article ID (``False`` by default)
|
|
697
760
|
:type full_response: bool
|
|
698
761
|
:returns: The API response or the ID of the article draft
|
|
699
|
-
:raises: :py:exc:`ValueError`,
|
|
762
|
+
:raises: :py:exc:`ValueError`,
|
|
763
|
+
:py:exc:`TypeError`,
|
|
764
|
+
:py:exc:`RuntimeError`
|
|
700
765
|
"""
|
|
701
766
|
return knowledge_module.create_article(self.sfdc_object, article_data=article_data, sobject=sobject,
|
|
702
767
|
full_response=full_response)
|
|
@@ -714,7 +779,9 @@ class Salesforce(object):
|
|
|
714
779
|
:param include_status_code: Determines if the API response status code should be returned (``False`` by default)
|
|
715
780
|
:type include_status_code: bool
|
|
716
781
|
:returns: A Boolean indicating if the update operation was successful, and optionally the API response status code
|
|
717
|
-
:raises: :py:exc:`ValueError`,
|
|
782
|
+
:raises: :py:exc:`ValueError`,
|
|
783
|
+
:py:exc:`TypeError`,
|
|
784
|
+
:py:exc:`RuntimeError`
|
|
718
785
|
"""
|
|
719
786
|
return knowledge_module.update_article(self.sfdc_object, record_id=record_id, article_data=article_data,
|
|
720
787
|
sobject=sobject, include_status_code=include_status_code)
|
|
@@ -781,7 +848,9 @@ class Salesforce(object):
|
|
|
781
848
|
:param major_version: Determines if the published article should be a major version (``True`` by default)
|
|
782
849
|
:type major_version: bool
|
|
783
850
|
:returns: The API response from the POST request
|
|
784
|
-
:raises: :py:exc:`RuntimeError`,
|
|
851
|
+
:raises: :py:exc:`RuntimeError`,
|
|
852
|
+
:py:exc:`TypeError`,
|
|
853
|
+
:py:exc:`ValueError`
|
|
785
854
|
"""
|
|
786
855
|
return knowledge_module.publish_multiple_articles(self.sfdc_object, article_id_list=article_id_list,
|
|
787
856
|
major_version=major_version)
|
|
@@ -790,7 +859,7 @@ class Salesforce(object):
|
|
|
790
859
|
"""This method assigns a single data category for a knowledge article.
|
|
791
860
|
(`Reference <https://itsmemohit.medium.com/quick-win-15-salesforce-knowledge-rest-apis-bb0725b2040e>`_)
|
|
792
861
|
|
|
793
|
-
..
|
|
862
|
+
.. version-added:: 1.2.0
|
|
794
863
|
|
|
795
864
|
:param article_id: The ID of the article to update
|
|
796
865
|
:type article_id: str
|
|
@@ -809,7 +878,7 @@ class Salesforce(object):
|
|
|
809
878
|
"""This function archives a published knowledge article.
|
|
810
879
|
(`Reference <https://developer.salesforce.com/docs/atlas.en-us.knowledge_dev.meta/knowledge_dev/knowledge_REST_archive_master_version.htm>`_)
|
|
811
880
|
|
|
812
|
-
..
|
|
881
|
+
.. version-added:: 1.3.0
|
|
813
882
|
|
|
814
883
|
:param article_id: The ID of the article to archive
|
|
815
884
|
:type article_id: str
|
|
@@ -818,6 +887,18 @@ class Salesforce(object):
|
|
|
818
887
|
"""
|
|
819
888
|
return knowledge_module.archive_article(self.sfdc_object, article_id=article_id)
|
|
820
889
|
|
|
890
|
+
def delete_article_draft(self, version_id):
|
|
891
|
+
"""This function deletes an unpublished knowledge article draft.
|
|
892
|
+
|
|
893
|
+
.. version-added:: 1.4.0
|
|
894
|
+
|
|
895
|
+
:param version_id: The 15-character or 18-character ``Id`` (Knowledge Article Version ID) value
|
|
896
|
+
:type version_id: str
|
|
897
|
+
:returns: The API response from the DELETE request
|
|
898
|
+
:raises: :py:exc:`RuntimeError`
|
|
899
|
+
"""
|
|
900
|
+
return knowledge_module.delete_article_draft(self.sfdc_object, version_id=version_id)
|
|
901
|
+
|
|
821
902
|
|
|
822
903
|
def define_connection_info():
|
|
823
904
|
"""This function prompts the user for the connection information.
|
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: 30 Jan 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,19 @@ 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):
|
|
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
|
+
:returns: The API response from the DELETE request
|
|
544
|
+
:raises: :py:exc:`RuntimeError`
|
|
545
|
+
"""
|
|
546
|
+
endpoint = f'/services/data/{sfdc_object.version}/sobjects/Knowledge__kav/{version_id}'
|
|
547
|
+
return sfdc_object.delete(endpoint)
|
salespyforce/utils/core_utils.py
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
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: 30 Jan 2026
|
|
10
10
|
"""
|
|
11
11
|
|
|
12
12
|
import random
|
|
@@ -23,6 +23,9 @@ from .. import errors
|
|
|
23
23
|
# Initialize the logger for this module
|
|
24
24
|
logger = log_utils.initialize_logging(__name__)
|
|
25
25
|
|
|
26
|
+
# Define constants
|
|
27
|
+
SALESFORCE_ID_SUFFIX_ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ012345'
|
|
28
|
+
|
|
26
29
|
|
|
27
30
|
def url_encode(raw_string):
|
|
28
31
|
"""This function encodes a string for use in URLs.
|
|
@@ -60,7 +63,8 @@ def get_file_type(file_path):
|
|
|
60
63
|
:param file_path: The full path to the file
|
|
61
64
|
:type file_path: str
|
|
62
65
|
:returns: The file type in string format (e.g. ``yaml`` or ``json``)
|
|
63
|
-
:raises: :py:exc:`FileNotFoundError`,
|
|
66
|
+
:raises: :py:exc:`FileNotFoundError`,
|
|
67
|
+
:py:exc:`salespyforce.errors.exceptions.UnknownFileTypeError`
|
|
64
68
|
"""
|
|
65
69
|
file_type = 'unknown'
|
|
66
70
|
if os.path.isfile(file_path):
|
|
@@ -97,6 +101,44 @@ def get_random_string(length=32, prefix_string=""):
|
|
|
97
101
|
return f"{prefix_string}{''.join([random.choice(string.ascii_letters + string.digits) for _ in range(length)])}"
|
|
98
102
|
|
|
99
103
|
|
|
104
|
+
def get_18_char_id(record_id: str) -> str:
|
|
105
|
+
"""This function converts a 15-character Salesforce record ID to its 18-character case-insensitive form.
|
|
106
|
+
|
|
107
|
+
.. version-added:: 1.4.0
|
|
108
|
+
|
|
109
|
+
:param record_id: The Salesforce record ID to convert (or return unchanged if already 18 characters)
|
|
110
|
+
:type record_id: str
|
|
111
|
+
:returns: The 18-character Salesforce record ID
|
|
112
|
+
:raises: :py:exc:`ValueError`
|
|
113
|
+
"""
|
|
114
|
+
# Ensure the provided record ID is a string
|
|
115
|
+
if not isinstance(record_id, str):
|
|
116
|
+
raise ValueError("Salesforce ID must be a string")
|
|
117
|
+
|
|
118
|
+
# Return the record ID unchanged if it is already 18 characters in length
|
|
119
|
+
if len(record_id) == 18:
|
|
120
|
+
return record_id
|
|
121
|
+
|
|
122
|
+
# Ensure the record ID is a valid 15-character value
|
|
123
|
+
if len(record_id) != 15:
|
|
124
|
+
raise ValueError("Salesforce ID must be 15 or 18 characters long")
|
|
125
|
+
|
|
126
|
+
# Define the checksum suffix (additional 3 characters)
|
|
127
|
+
suffix = ""
|
|
128
|
+
for i in range(0, 15, 5):
|
|
129
|
+
chunk = record_id[i:i + 5]
|
|
130
|
+
bitmask = 0
|
|
131
|
+
|
|
132
|
+
for index, char in enumerate(chunk):
|
|
133
|
+
if "A" <= char <= "Z":
|
|
134
|
+
bitmask |= 1 << index
|
|
135
|
+
|
|
136
|
+
suffix += SALESFORCE_ID_SUFFIX_ALPHABET[bitmask]
|
|
137
|
+
|
|
138
|
+
# Return the 18-character ID value
|
|
139
|
+
return record_id + suffix
|
|
140
|
+
|
|
141
|
+
|
|
100
142
|
def get_image_ref_id(image_url):
|
|
101
143
|
"""This function parses an image URL to identify the reference ID (refid) value.
|
|
102
144
|
(`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
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
:Example: ``logger = log_utils.initialize_logging(__name__)``
|
|
7
7
|
:Created By: Jeff Shurtliff
|
|
8
8
|
:Last Modified: Jeff Shurtliff
|
|
9
|
-
:Modified Date:
|
|
9
|
+
:Modified Date: 31 Dec 2025
|
|
10
10
|
"""
|
|
11
11
|
|
|
12
12
|
import os
|
|
@@ -59,7 +59,7 @@ class LessThanFilter(logging.Filter):
|
|
|
59
59
|
self.max_level = exclusive_maximum
|
|
60
60
|
|
|
61
61
|
def filter(self, record):
|
|
62
|
-
"""This method returns a Boolean integer value indicating whether
|
|
62
|
+
"""This method returns a Boolean integer value indicating whether a message should be logged.
|
|
63
63
|
|
|
64
64
|
.. note:: A non-zero return indicates that the message will be logged.
|
|
65
65
|
"""
|
|
@@ -69,6 +69,9 @@ class LessThanFilter(logging.Filter):
|
|
|
69
69
|
def _apply_defaults(_logger_name, _formatter, _debug, _log_level, _file_level, _console_level, _syslog_level):
|
|
70
70
|
"""This function applies default values to the configuration settings if not explicitly defined.
|
|
71
71
|
|
|
72
|
+
.. version-changed:: 1.4.0
|
|
73
|
+
The default logging level is now defined.
|
|
74
|
+
|
|
72
75
|
:param _logger_name: The name of the logger instance
|
|
73
76
|
:type _logger_name: str, None
|
|
74
77
|
:param _formatter: The log format to utilize for the logger instance
|
|
@@ -79,8 +82,9 @@ def _apply_defaults(_logger_name, _formatter, _debug, _log_level, _file_level, _
|
|
|
79
82
|
:type _log_level: str, None
|
|
80
83
|
:returns: The values that will be used for the configuration settings
|
|
81
84
|
"""
|
|
85
|
+
_default_log_level = LOGGING_DEFAULTS.get('log_level')
|
|
82
86
|
_log_levels = {
|
|
83
|
-
'general': _log_level,
|
|
87
|
+
'general': _log_level or _default_log_level,
|
|
84
88
|
'file': _file_level,
|
|
85
89
|
'console': _console_level,
|
|
86
90
|
'syslog': _syslog_level,
|
|
@@ -90,12 +94,9 @@ def _apply_defaults(_logger_name, _formatter, _debug, _log_level, _file_level, _
|
|
|
90
94
|
for _log_type in _log_levels:
|
|
91
95
|
_log_levels[_log_type] = 'debug'
|
|
92
96
|
else:
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
_log_levels[_lvl_type] = _log_level
|
|
97
|
-
else:
|
|
98
|
-
_log_level = LOGGING_DEFAULTS.get('log_level')
|
|
97
|
+
for _lvl_type, _lvl_value in _log_levels.items():
|
|
98
|
+
if _lvl_value is None:
|
|
99
|
+
_log_levels[_lvl_type] = _log_levels['general']
|
|
99
100
|
if _formatter and isinstance(_formatter, str):
|
|
100
101
|
_formatter = logging.Formatter(_formatter)
|
|
101
102
|
_formatter = LOGGING_DEFAULTS.get('formatter') if not _formatter else _formatter
|
|
@@ -120,7 +121,7 @@ def _set_logging_level(_logger, _log_level):
|
|
|
120
121
|
"""This function sets the logging level for a :py:class:`logging.Logger` instance.
|
|
121
122
|
|
|
122
123
|
:param _logger: The :py:class:`logging.Logger` instance
|
|
123
|
-
:type _logger: Logger
|
|
124
|
+
:type _logger: class[logging.Logger]
|
|
124
125
|
:param _log_level: The log level as a string (``debug``, ``info``, ``warning``, ``error`` or ``critical``)
|
|
125
126
|
:type _log_level: str
|
|
126
127
|
:returns: The :py:class:`logging.Logger` instance with a logging level set where applicable
|
|
@@ -161,7 +162,7 @@ def _add_file_handler(_logger, _log_level, _log_file, _overwrite, _formatter):
|
|
|
161
162
|
"""This function adds a :py:class:`logging.FileHandler` to the :py:class:`logging.Logger` instance.
|
|
162
163
|
|
|
163
164
|
:param _logger: The :py:class:`logging.Logger` instance
|
|
164
|
-
:type _logger: Logger
|
|
165
|
+
:type _logger: class[logging.Logger]
|
|
165
166
|
:param _log_level: The log level to set for the handler
|
|
166
167
|
:type _log_level: str
|
|
167
168
|
:param _log_file: The log file (as a file name or a file path) to which messages should be written
|
|
@@ -173,7 +174,7 @@ def _add_file_handler(_logger, _log_level, _log_file, _overwrite, _formatter):
|
|
|
173
174
|
:param _overwrite: Determines if messages should be appended to the file (default) or overwrite it
|
|
174
175
|
:type _overwrite: bool
|
|
175
176
|
:param _formatter: The :py:class:`logging.Formatter` to apply to messages passed through the handler
|
|
176
|
-
:type _formatter: Formatter
|
|
177
|
+
:type _formatter: class[logging.Formatter]
|
|
177
178
|
:returns: The :py:class:`logging.Logger` instance with the added :py:class:`logging.FileHandler`
|
|
178
179
|
"""
|
|
179
180
|
# Define the log file to use
|
|
@@ -202,11 +203,11 @@ def _add_stream_handler(_logger, _log_level, _formatter):
|
|
|
202
203
|
"""This function adds a :py:class:`logging.StreamHandler` to the :py:class:`logging.Logger` instance.
|
|
203
204
|
|
|
204
205
|
:param _logger: The :py:class:`logging.Logger` instance
|
|
205
|
-
:type _logger: Logger
|
|
206
|
+
:type _logger: class[logging.Logger]
|
|
206
207
|
:param _log_level: The log level to set for the handler
|
|
207
208
|
:type _log_level: str
|
|
208
209
|
:param _formatter: The :py:class:`logging.Formatter` to apply to messages passed through the handler
|
|
209
|
-
:type _formatter: Formatter
|
|
210
|
+
:type _formatter: class[logging.Formatter]
|
|
210
211
|
:returns: The :py:class:`logging.Logger` instance with the added :py:class:`logging.StreamHandler`
|
|
211
212
|
"""
|
|
212
213
|
_log_level = HANDLER_DEFAULTS.get('console_log_level') if not _log_level else _log_level
|
|
@@ -228,11 +229,11 @@ def _add_split_stream_handlers(_logger, _log_level, _formatter):
|
|
|
228
229
|
more information on how this filtering is implemented and for credit to the original author.
|
|
229
230
|
|
|
230
231
|
:param _logger: The :py:class:`logging.Logger` instance
|
|
231
|
-
:type _logger: Logger
|
|
232
|
+
:type _logger: class[logging.Logger]
|
|
232
233
|
:param _log_level: The log level provided for the stream handler (i.e. console output)
|
|
233
234
|
:type _log_level: str
|
|
234
235
|
:param _formatter: The :py:class:`logging.Formatter` to apply to messages passed through the handlers
|
|
235
|
-
:type _formatter: Formatter
|
|
236
|
+
:type _formatter: class[logging.Formatter]
|
|
236
237
|
:returns: The logger instance with the two handlers added
|
|
237
238
|
"""
|
|
238
239
|
# Configure and add the STDOUT handler
|