salespyforce 1.4.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,53 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ :Module: salespyforce
4
+ :Synopsis: This is the ``__init__`` module for the salespyforce package
5
+ :Created By: Jeff Shurtliff
6
+ :Last Modified: Jeff Shurtliff
7
+ :Modified Date: 08 May 2023
8
+ """
9
+
10
+ from . import core
11
+ from .core import Salesforce
12
+ from .utils import version
13
+
14
+ __all__ = ['core', 'Salesforce']
15
+
16
+ # Define the package version by pulling from the highspot.utils.version module
17
+ __version__ = version.get_full_version()
18
+
19
+
20
+ # Allow the core.define_connection_info() function to be executed directly
21
+ def define_connection_info():
22
+ """This function prompts the user for the connection information.
23
+
24
+ :returns: The connection info in a dictionary
25
+ """
26
+ return core.define_connection_info()
27
+
28
+
29
+ # Allow the core.compile_connection_info() function to be executed directly
30
+ def compile_connection_info(base_url, org_id, username, password, endpoint_url,
31
+ client_id, client_secret, security_token):
32
+ """This function compiles the connection info into a dictionary that can be consumed by the core object.
33
+
34
+ :param base_url: The base URL of the Salesforce instance
35
+ :type base_url: str
36
+ :param org_id: The Org ID of the Salesforce instance
37
+ :type org_id: str
38
+ :param username: The username of the API user
39
+ :type username: str
40
+ :param password: The password of the API user
41
+ :type password: str
42
+ :param endpoint_url: The endpoint URL for the Salesforce instance
43
+ :type endpoint_url: str
44
+ :param client_id: The Client ID for the Salesforce instance
45
+ :type client_id: str
46
+ :param client_secret: The Client Secret for the Salesforce instance
47
+ :type client_secret: str
48
+ :param security_token: The Security Token for the Salesforce instance
49
+ :type security_token: str
50
+ :returns: The connection info in a dictionary
51
+ """
52
+ return core.compile_connection_info(base_url, org_id, username, password, endpoint_url,
53
+ client_id, client_secret, security_token)
salespyforce/api.py ADDED
@@ -0,0 +1,272 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ :Module: salespyforce.api
4
+ :Synopsis: Defines the basic functions associated with the Salesforce API
5
+ :Created By: Jeff Shurtliff
6
+ :Last Modified: Jeff Shurtliff
7
+ :Modified Date: 03 Feb 2026
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from typing import Optional
13
+
14
+ import requests
15
+
16
+ from . import errors
17
+ from .utils import core_utils, log_utils
18
+
19
+ # Define constants
20
+ DEFAULT_API_REQUEST_TIMEOUT = 30
21
+
22
+ # Initialize logging
23
+ logger = log_utils.initialize_logging(__name__)
24
+
25
+
26
+ def get(
27
+ sfdc_object,
28
+ endpoint: str,
29
+ params: Optional[dict] = None,
30
+ headers: Optional[dict] = None,
31
+ timeout: Optional[int] = None,
32
+ show_full_error: bool = True,
33
+ return_json: bool = True,
34
+ ):
35
+ """This method performs a GET request against the Salesforce instance.
36
+ (`Reference <https://jereze.com/code/authentification-salesforce-rest-api-python/>`_)
37
+
38
+ .. versionchanged:: 1.4.0
39
+ The full URL for the API call is now constructed prior to making the call. The provided URL is also
40
+ now evaluated to ensure it is a valid Salesforce URL. Additionally, a global constant is now leveraged
41
+ for the API timeout value instead of hardcoding the value. (Timeout is still **30** seconds in this version)
42
+
43
+ :param sfdc_object: The instantiated SalesPyForce object
44
+ :type sfdc_object: class[salespyforce.Salesforce]
45
+ :param endpoint: The API endpoint to query
46
+ :type endpoint: str
47
+ :param params: The query parameters (where applicable)
48
+ :type params: dict, None
49
+ :param headers: Specific API headers to use when performing the API call
50
+ :type headers: dict, None
51
+ :param timeout: The timeout period in seconds (defaults to ``30``)
52
+ :type timeout: int, None
53
+ :param show_full_error: Determines if the full error message should be displayed (defaults to ``True``)
54
+ :type show_full_error: bool
55
+ :param return_json: Determines if the response should be returned in JSON format (defaults to ``True``)
56
+ :returns: The API response in JSON format or as a ``requests`` object
57
+ :raises: :py:exc:`TypeError`,
58
+ :py:exc:`RuntimeError`,
59
+ :py:exc:`salespyforce.errors.exceptions.InvalidURLError`
60
+ """
61
+ # Define the parameters as an empty dictionary if none are provided
62
+ params = {} if params is None else params
63
+
64
+ # Define the headers
65
+ default_headers = _get_headers(sfdc_object.access_token)
66
+ headers = default_headers if not headers else headers
67
+
68
+ # Construct the request URL
69
+ url = _construct_full_query_url(endpoint, sfdc_object.instance_url)
70
+
71
+ # Define the API request timeout (using default value if not explicitly defined with parameter)
72
+ timeout = DEFAULT_API_REQUEST_TIMEOUT if not timeout else timeout
73
+
74
+ # Perform the API call
75
+ response = requests.get(url, headers=headers, params=params, timeout=timeout)
76
+ if response.status_code >= 300:
77
+ # TODO: Functionalize this segment and figure out how to improve on the approach somehow
78
+ if show_full_error:
79
+ raise RuntimeError(f'The GET request failed with a {response.status_code} status code.\n'
80
+ f'{response.text}')
81
+ else:
82
+ raise RuntimeError(f'The GET request failed with a {response.status_code} status code.')
83
+ # TODO: Leverage private function for this section across all API call functions (see TODO in api_call_with_payload)
84
+ if return_json:
85
+ response = response.json()
86
+ return response
87
+
88
+
89
+ def api_call_with_payload(
90
+ sfdc_object,
91
+ method: str,
92
+ endpoint: str,
93
+ payload: dict,
94
+ params: Optional[dict] = None,
95
+ headers: Optional[dict] = None,
96
+ timeout: Optional[int] = None,
97
+ show_full_error: bool = True,
98
+ return_json: bool = True,
99
+ ):
100
+ """This method performs a POST call against the Salesforce instance.
101
+ (`Reference <https://jereze.com/code/authentification-salesforce-rest-api-python/>`_)
102
+
103
+ .. versionchanged:: 1.4.0
104
+ The full URL for the API call is now constructed prior to making the call. The provided URL is also
105
+ now evaluated to ensure it is a valid Salesforce URL. Additionally, a global constant is now leveraged
106
+ for the API timeout value instead of hardcoding the value. (Timeout is still **30** seconds in this version)
107
+
108
+ :param sfdc_object: The instantiated SalesPyForce object
109
+ :type sfdc_object: class[salespyforce.Salesforce]
110
+ :param method: The API method (``post``, ``put``, or ``patch``)
111
+ :type method: str
112
+ :param endpoint: The API endpoint to query
113
+ :type endpoint: str
114
+ :param payload: The payload to leverage in the API call
115
+ :type payload: dict
116
+ :param params: The query parameters (where applicable)
117
+ :type params: dict, None
118
+ :param headers: Specific API headers to use when performing the API call
119
+ :type headers: dict, None
120
+ :param timeout: The timeout period in seconds (defaults to ``30``)
121
+ :type timeout: int, None
122
+ :param show_full_error: Determines if the full error message should be displayed (defaults to ``True``)
123
+ :type show_full_error: bool
124
+ :param return_json: Determines if the response should be returned in JSON format (defaults to ``True``)
125
+ :returns: The API response in JSON format or as a ``requests`` object
126
+ :raises: :py:exc:`TypeError`,
127
+ :py:exc:`RuntimeError`,
128
+ :py:exc:`ValueError`,
129
+ :py:exc:`salespyforce.errors.exceptions.InvalidURLError`
130
+ """
131
+ # Define the parameters as an empty dictionary if none are provided
132
+ params = {} if params is None else params
133
+
134
+ # Define the headers
135
+ default_headers = _get_headers(sfdc_object.access_token)
136
+ headers = default_headers if not headers else headers
137
+
138
+ # Construct the request URL
139
+ url = _construct_full_query_url(endpoint, sfdc_object.instance_url)
140
+
141
+ # Define the API request timeout (using default value if not explicitly defined with parameter)
142
+ timeout = DEFAULT_API_REQUEST_TIMEOUT if not timeout else timeout
143
+
144
+ # Perform the API call
145
+ if method.lower() == 'post':
146
+ response = requests.post(url, json=payload, headers=headers, params=params, timeout=timeout)
147
+ elif method.lower() == 'patch':
148
+ response = requests.patch(url, json=payload, headers=headers, params=params, timeout=timeout)
149
+ elif method.lower() == 'put':
150
+ response = requests.put(url, json=payload, headers=headers, params=params, timeout=timeout)
151
+ else:
152
+ raise ValueError('The API call method (POST or PATCH or PUT) must be defined')
153
+
154
+ # Examine the result
155
+ if response.status_code >= 300:
156
+ if show_full_error:
157
+ # TODO: Functionalize this segment and figure out how to improve on the approach somehow
158
+ raise RuntimeError(f'The POST request failed with a {response.status_code} status code.\n'
159
+ f'{response.text}')
160
+ else:
161
+ raise RuntimeError(f'The POST request failed with a {response.status_code} status code.')
162
+ # TODO: Break this out into a separate private function so it can be reused and standardized
163
+ if return_json:
164
+ try:
165
+ response = response.json()
166
+ except Exception as exc:
167
+ # TODO: log the exception rather than using a print statement
168
+ print(f'Failed to convert the API response to JSON format due to the following exception: {exc}')
169
+ return response
170
+
171
+
172
+ def delete(
173
+ sfdc_object,
174
+ endpoint: str,
175
+ params: Optional[dict] = None,
176
+ headers: Optional[dict] = None,
177
+ timeout: Optional[int] = None,
178
+ show_full_error: bool = True,
179
+ return_json: bool = True):
180
+ """This method performs a DELETE request against the Salesforce instance.
181
+
182
+ .. versionadded:: 1.4.0
183
+
184
+ :param sfdc_object: The instantiated SalesPyForce object
185
+ :type sfdc_object: class[salespyforce.Salesforce]
186
+ :param endpoint: The API endpoint to query
187
+ :type endpoint: str
188
+ :param params: The query parameters (where applicable)
189
+ :type params: dict, None
190
+ :param headers: Specific API headers to use when performing the API call
191
+ :type headers: dict, None
192
+ :param timeout: The timeout period in seconds (defaults to ``30``)
193
+ :type timeout: int, None
194
+ :param show_full_error: Determines if the full error message should be displayed (defaults to ``True``)
195
+ :type show_full_error: bool
196
+ :param return_json: Determines if the response should be returned in JSON format (defaults to ``True``)
197
+ :returns: The API response in JSON format or as a ``requests`` object
198
+ :raises: :py:exc:`TypeError`,
199
+ :py:exc:`RuntimeError`,
200
+ :py:exc:`salespyforce.errors.exceptions.InvalidURLError`
201
+ """
202
+ # Define the parameters as an empty dictionary if none are provided
203
+ params = {} if params is None else params
204
+
205
+ # Define the headers
206
+ default_headers = _get_headers(sfdc_object.access_token)
207
+ headers = default_headers if not headers else headers
208
+
209
+ # Construct the request URL
210
+ url = _construct_full_query_url(endpoint, sfdc_object.instance_url)
211
+
212
+ # Define the API request timeout (using default value if not explicitly defined with parameter)
213
+ timeout = DEFAULT_API_REQUEST_TIMEOUT if not timeout else timeout
214
+
215
+ # Perform the API call
216
+ response = requests.delete(url, headers=headers, params=params, timeout=timeout)
217
+ if response.status_code >= 300:
218
+ if show_full_error:
219
+ # TODO: Functionalize this segment and figure out how to improve on the approach somehow
220
+ raise RuntimeError(f'The DELETE request failed with a {response.status_code} status code.\n'
221
+ f'{response.text}')
222
+ else:
223
+ raise RuntimeError(f'The DELETE request failed with a {response.status_code} status code.')
224
+ # TODO: Leverage private function for this section across all API call functions (see TODO in api_call_with_payload)
225
+ if return_json:
226
+ response = response.json()
227
+ return response
228
+
229
+
230
+ def _get_headers(_access_token: str, _header_type: str = 'default') -> dict:
231
+ """This function returns the appropriate HTTP headers to use for different types of API calls."""
232
+ headers = {
233
+ 'content-type': 'application/json',
234
+ 'accept-encoding': 'gzip',
235
+ 'authorization': f'Bearer {_access_token}'
236
+ }
237
+ if _header_type == 'articles':
238
+ headers['accept-language'] = 'en-US'
239
+ return headers
240
+
241
+
242
+ def _construct_full_query_url(_endpoint: str, _instance_url: str) -> str:
243
+ """This function constructs the URL to use in an API call to the Salesforce REST API.
244
+
245
+ .. versionadded:: 1.4.0
246
+
247
+ :param _endpoint: The endpoint provided when calling an API call method or function
248
+ :type _endpoint: str
249
+ :param _instance_url: The Salesforce instance URL defined when the core object was instantiated
250
+ :type _instance_url: str
251
+ :returns: The fully qualified URL
252
+ :raises: :py:exc:`TypeError`,
253
+ :py:exc:`salespyforce.errors.exceptions.InvalidURLError`
254
+ """
255
+ # Raise an exception if the endpoint is not a string
256
+ if not isinstance(_endpoint, str):
257
+ _exc_msg = 'The provided URL must be a string and a valid Salesforce URL'
258
+ logger.critical(_exc_msg)
259
+ raise TypeError(_exc_msg)
260
+
261
+ # Construct the URL as needed by prepending the instance URL
262
+ if _endpoint.startswith('https://'):
263
+ # Only permit valid Salesforce URLs
264
+ if not core_utils.is_valid_salesforce_url(_endpoint):
265
+ raise errors.exceptions.InvalidURLError(url=_endpoint)
266
+ _url = _endpoint
267
+ else:
268
+ _endpoint = f'/{_endpoint}' if not _endpoint.startswith('/') else _endpoint
269
+ _url = f'{_instance_url}{_endpoint}'
270
+
271
+ # Return the constructed URL
272
+ return _url
@@ -0,0 +1,192 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ :Module: salespyforce.chatter
4
+ :Synopsis: Defines the Chatter-related functions associated with the Salesforce Connect API
5
+ :Created By: Jeff Shurtliff
6
+ :Last Modified: Jeff Shurtliff
7
+ :Modified Date: 03 Feb 2026
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from typing import Optional
13
+
14
+ from . import errors
15
+ from .utils import log_utils
16
+
17
+ # Initialize logging
18
+ logger = log_utils.initialize_logging(__name__)
19
+
20
+
21
+ def _get_site_endpoint_segment(_site_id: Optional[str] = None) -> str:
22
+ """This function constructs the endpoint segment when querying a specific Experience Cloud site.
23
+
24
+ :param _site_id: The Site ID of the Experience Cloud site
25
+ :type _site_id: str, None
26
+ :returns: The API endpoint segment (or a blank string if no Site ID was provided)
27
+ """
28
+ _endpoint_segment = f'/connect/communities/{_site_id}' if _site_id else ''
29
+ return _endpoint_segment
30
+
31
+
32
+ def get_my_news_feed(sfdc_object, site_id: Optional[str] = None):
33
+ """This function retrieves the news feed for the user calling the function.
34
+ (`Reference <https://developer.salesforce.com/docs/atlas.en-us.chatterapi.meta/chatterapi/quickreference_get_news_feed.htm>`_)
35
+
36
+ :param sfdc_object: The instantiated SalesPyForce object
37
+ :type sfdc_object: class[salespyforce.Salesforce]
38
+ :param site_id: The ID of an Experience Cloud site against which to query (optional)
39
+ :type site_id: str, None
40
+ :returns: The news feed data
41
+ :raises: :py:exc:`RuntimeError`
42
+ """
43
+ site_segment = _get_site_endpoint_segment(site_id)
44
+ endpoint = f'/services/data/{sfdc_object.version}{site_segment}/chatter/feeds/news/me/feed-elements'
45
+ return sfdc_object.get(endpoint)
46
+
47
+
48
+ def get_user_news_feed(sfdc_object, user_id: str, site_id: Optional[str] = None):
49
+ """This function retrieves another user's news feed.
50
+ (`Reference <https://developer.salesforce.com/docs/atlas.en-us.chatterapi.meta/chatterapi/quickreference_get_user_profile_feed.htm>`_)
51
+
52
+ :param sfdc_object: The instantiated SalesPyForce object
53
+ :type sfdc_object: class[salespyforce.Salesforce]
54
+ :param user_id: The ID of the user whose feed you wish to return
55
+ :type user_id: str
56
+ :param site_id: The ID of an Experience Cloud site against which to query (optional)
57
+ :type site_id: str, None
58
+ :returns: The news feed data
59
+ :raises: :py:exc:`RuntimeError`
60
+ """
61
+ site_segment = _get_site_endpoint_segment(site_id)
62
+ endpoint = f'/services/data/{sfdc_object.version}{site_segment}/chatter/feeds/user-profile/{user_id}/feed-elements'
63
+ return sfdc_object.get(endpoint)
64
+
65
+
66
+ def get_group_feed(sfdc_object, group_id: str, site_id: Optional[str] = None):
67
+ """This function retrieves a group's news feed.
68
+ (`Reference <https://developer.salesforce.com/docs/atlas.en-us.chatterapi.meta/chatterapi/quickreference_get_group_feed.htm>`_)
69
+
70
+ :param sfdc_object: The instantiated SalesPyForce object
71
+ :type sfdc_object: class[salespyforce.Salesforce]
72
+ :param group_id: The ID of the group whose feed you wish to return
73
+ :type group_id: str
74
+ :param site_id: The ID of an Experience Cloud site against which to query (optional)
75
+ :type site_id: str, None
76
+ :returns: The news feed data
77
+ :raises: :py:exc:`RuntimeError`
78
+ """
79
+ site_segment = _get_site_endpoint_segment(site_id)
80
+ endpoint = f'/services/data/{sfdc_object.version}{site_segment}/chatter/feeds/record/{group_id}/feed-elements'
81
+ return sfdc_object.get(endpoint)
82
+
83
+
84
+ def post_feed_item(
85
+ sfdc_object,
86
+ subject_id: str,
87
+ message_text: Optional[str] = None,
88
+ message_segments: Optional[list] = None,
89
+ site_id: Optional[str] = None,
90
+ created_by_id: Optional[str] = None,
91
+ ):
92
+ """This function publishes a new Chatter feed item.
93
+ (`Reference <https://developer.salesforce.com/docs/atlas.en-us.chatterapi.meta/chatterapi/quickreference_post_feed_item.htm>`_)
94
+
95
+ .. versionchanged:: 1.4.0
96
+ The function now raises the :py:exc:`salespyforce.errors.exceptions.MissingRequiredDataError` exception rather than the
97
+ generic :py:exc:`RuntimeError` exception.
98
+
99
+ :param sfdc_object: The instantiated SalesPyForce object
100
+ :type sfdc_object: class[salespyforce.Salesforce]
101
+ :param subject_id: The Subject ID against which to publish the feed item (e.g. ``0F9B000000000W2``)
102
+ :type subject_id: str
103
+ :param message_text: Plaintext to be used as the message body
104
+ :type message_segments: str, None
105
+ :param message_segments: Collection of message segments to use instead of a plaintext message
106
+ :type message_segments: list, None
107
+ :param site_id: The ID of an Experience Cloud site against which to query (optional)
108
+ :type site_id: str, None
109
+ :param created_by_id: The ID of the user to impersonate (**Experimental**)
110
+ :type created_by_id: str, None
111
+ :returns: The response of the POST request
112
+ :raises: :py:exc:`RuntimeError`,
113
+ :py:exc:`salespyforce.errors.exceptions.MissingRequiredDataError`
114
+ """
115
+ site_segment = _get_site_endpoint_segment(site_id)
116
+ if not any((message_text, message_segments)):
117
+ raise errors.exceptions.MissingRequiredDataError('Message text or message segments are required to post a feed item.')
118
+ if not message_segments:
119
+ message_segments = _construct_simple_message_segment(message_text)
120
+ body = {
121
+ 'body': {
122
+ 'messageSegments': message_segments
123
+ },
124
+ 'feedElementType': 'FeedItem',
125
+ 'subjectId': subject_id,
126
+ }
127
+ if created_by_id:
128
+ body['createdById'] = created_by_id
129
+ endpoint = f'/services/data/{sfdc_object.version}{site_segment}/chatter/feed-elements?' \
130
+ f'feedElementType=FeedItem&subjectId={subject_id}'
131
+ return sfdc_object.post(endpoint=endpoint, payload=body)
132
+
133
+
134
+ def post_comment(
135
+ sfdc_object,
136
+ feed_element_id: str,
137
+ message_text: Optional[str] = None,
138
+ message_segments: Optional[list] = None,
139
+ site_id: Optional[str] = None,
140
+ created_by_id: Optional[str] = None,
141
+ ):
142
+ """This function publishes a comment on a Chatter feed item.
143
+ (`Reference <https://developer.salesforce.com/docs/atlas.en-us.chatterapi.meta/chatterapi/quickreference_post_comment_to_feed_element.htm>`_)
144
+
145
+ :param sfdc_object: The instantiated SalesPyForce object
146
+ :type sfdc_object: class[salespyforce.Salesforce]
147
+ :param feed_element_id: The ID of the feed element on which to post the comment
148
+ :type feed_element_id: str
149
+ :param message_text: Plaintext to be used as the message body
150
+ :type message_text: str, None
151
+ :param message_segments: Collection of message segments to use instead of a plaintext message
152
+ :type message_segments: list, None
153
+ :param site_id: The ID of an Experience Cloud site against which to query (optional)
154
+ :type site_id: str, None
155
+ :param created_by_id: The ID of the user to impersonate (**Experimental**)
156
+ :type created_by_id: str, None
157
+ :returns: The response of the POST request
158
+ :raises: :py:exc:`RuntimeError`
159
+ """
160
+ site_segment = _get_site_endpoint_segment(site_id)
161
+ if not any((message_text, message_segments)):
162
+ raise RuntimeError('Message text or message segments are required to post a feed comment.')
163
+
164
+ if not message_segments:
165
+ message_segments = _construct_simple_message_segment(message_text)
166
+ body = {
167
+ 'body': {
168
+ 'messageSegments': message_segments
169
+ }
170
+ }
171
+ if created_by_id:
172
+ # noinspection PyTypeChecker
173
+ body['createdById'] = created_by_id
174
+ endpoint = f'/services/data/{sfdc_object.version}{site_segment}/chatter/feed-elements/' \
175
+ f'{feed_element_id}/capabilities/comments/items'
176
+ return sfdc_object.post(endpoint=endpoint, payload=body)
177
+
178
+
179
+ def _construct_simple_message_segment(_message_text: str) -> list:
180
+ """This function constructs a simple message segments collection to be used in an API payload.
181
+
182
+ :param _message_text: The plaintext message to be embedded in a message segment.
183
+ :type _message_text: str
184
+ :returns: The constructed message segments payload
185
+ """
186
+ _message_segments = [
187
+ {
188
+ 'type': 'text',
189
+ 'text': _message_text
190
+ }
191
+ ]
192
+ return _message_segments