sfq 0.0.12__tar.gz → 0.0.14__tar.gz

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.
sfq-0.0.14/PKG-INFO ADDED
@@ -0,0 +1,179 @@
1
+ Metadata-Version: 2.4
2
+ Name: sfq
3
+ Version: 0.0.14
4
+ Summary: Python wrapper for the Salesforce's Query API.
5
+ Author-email: David Moruzzi <sfq.pypi@dmoruzi.com>
6
+ Keywords: salesforce,salesforce query
7
+ Classifier: Development Status :: 3 - Alpha
8
+ Classifier: Intended Audience :: Developers
9
+ Classifier: Programming Language :: Python :: 3.9
10
+ Classifier: Programming Language :: Python :: 3.10
11
+ Classifier: Programming Language :: Python :: 3.12
12
+ Classifier: Programming Language :: Python :: 3.13
13
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
14
+ Requires-Python: >=3.9
15
+ Description-Content-Type: text/markdown
16
+
17
+ # sfq (Salesforce Query)
18
+
19
+ sfq is a lightweight Python wrapper library designed to simplify querying Salesforce, reducing repetitive code for accessing Salesforce data.
20
+
21
+ For more varied workflows, consider using an alternative like [Simple Salesforce](https://simple-salesforce.readthedocs.io/en/stable/). This library was even referenced on the [Salesforce Developers Blog](https://developer.salesforce.com/blogs/2021/09/how-to-automate-data-extraction-from-salesforce-using-python).
22
+
23
+ ## Features
24
+
25
+ - Simplified query execution for Salesforce instances.
26
+ - Integration with Salesforce authentication via refresh tokens.
27
+ - Option to interact with Salesforce Tooling API for more advanced queries.
28
+
29
+ ## Installation
30
+
31
+ You can install the `sfq` library using `pip`:
32
+
33
+ ```bash
34
+ pip install sfq
35
+ ```
36
+
37
+ ## Usage
38
+
39
+ ### Library Querying
40
+
41
+ ```python
42
+ from sfq import SFAuth
43
+
44
+ # Initialize the SFAuth class with authentication details
45
+ sf = SFAuth(
46
+ instance_url="https://example-dev-ed.trailblaze.my.salesforce.com",
47
+ client_id="your-client-id-here",
48
+ client_secret="your-client-secret-here",
49
+ refresh_token="your-refresh-token-here"
50
+ )
51
+
52
+ # Execute a query to fetch account records
53
+ print(sf.query("SELECT Id FROM Account LIMIT 5"))
54
+
55
+ # Execute a query to fetch Tooling API data
56
+ print(sf.tooling_query("SELECT Id, FullName, Metadata FROM SandboxSettings LIMIT 5"))
57
+ ```
58
+
59
+ ### sObject Key Prefixes
60
+
61
+ ```python
62
+ # Key prefix via IDs
63
+ print(sf.get_sobject_prefixes())
64
+ >>> {'0Pp': 'AIApplication', '6S9': 'AIApplicationConfig', '9qd': 'AIInsightAction', '9bq': 'AIInsightFeedback', '0T2': 'AIInsightReason', '9qc': 'AIInsightValue', ...}
65
+
66
+ # Key prefix via names
67
+ print(sf.get_sobject_prefixes(key_type="name"))
68
+ >>> {'AIApplication': '0Pp', 'AIApplicationConfig': '6S9', 'AIInsightAction': '9qd', 'AIInsightFeedback': '9bq', 'AIInsightReason': '0T2', 'AIInsightValue': '9qc', ...}
69
+ ```
70
+
71
+ ### Composite Batch Queries
72
+
73
+ ```python
74
+ multiple_queries = {
75
+ "Recent Users": """
76
+ SELECT Id, Name,CreatedDate
77
+ FROM User
78
+ ORDER BY CreatedDate DESC
79
+ LIMIT 10""",
80
+ "Recent Accounts": "SELECT Id, Name, CreatedDate FROM Account ORDER BY CreatedDate DESC LIMIT 10",
81
+ "Frozen Users": "SELECT Id, UserId FROM UserLogin WHERE IsFrozen = true", # If exceeds 2000 records, will paginate
82
+ }
83
+
84
+ batched_response = sf.cquery(multiple_queries)
85
+
86
+ for subrequest_identifer, subrequest_response in batched_response.items():
87
+ print(f'"{subrequest_identifer}" returned {subrequest_response["totalSize"]} records')
88
+ >>> "Recent Users" returned 10 records
89
+ >>> "Recent Accounts" returned 10 records
90
+ >>> "Frozen Users" returned 4082 records
91
+ ```
92
+
93
+ ### Static Resources
94
+
95
+ ```python
96
+ page = sf.read_static_resource_id('081aj000009jUMXAA2')
97
+ print(f'Initial resource: {page}')
98
+ >>> Initial resource: <h1>It works!</h1>
99
+ sf.update_static_resource_name('HelloWorld', '<h1>Hello World</h1>')
100
+ page = sf.read_static_resource_name('HelloWorld')
101
+ print(f'Updated resource: {page}')
102
+ >>> Updated resource: <h1>Hello World</h1>
103
+ sf.update_static_resource_id('081aj000009jUMXAA2', '<h1>It works!</h1>')
104
+ ```
105
+
106
+ ## How to Obtain Salesforce Tokens
107
+
108
+ To use the `sfq` library, you'll need a **client ID** and **refresh token**. The easiest way to obtain these is by using the Salesforce CLI:
109
+
110
+ ### Steps to Get Tokens
111
+
112
+ 1. **Install the Salesforce CLI**:
113
+ Follow the instructions on the [Salesforce CLI installation page](https://developer.salesforce.com/tools/salesforcecli).
114
+
115
+ 2. **Authenticate with Salesforce**:
116
+ Login to your Salesforce org using the following command:
117
+
118
+ ```bash
119
+ sf org login web --alias int --instance-url https://corpa--int.sandbox.my.salesforce.com
120
+ ```
121
+
122
+ 3. **Display Org Details**:
123
+ To get the client ID, client secret, refresh token, and instance URL, run:
124
+
125
+ ```bash
126
+ sf org display --target-org int --verbose --json
127
+ ```
128
+
129
+ The output will look like this:
130
+
131
+ ```json
132
+ {
133
+ "status": 0,
134
+ "result": {
135
+ "id": "00Daa0000000000000",
136
+ "apiVersion": "63.0",
137
+ "accessToken": "00Daa0000000000000!evaU3fYZEWGUrqI5rMtaS8KYbHfeqA7YWzMgKToOB43Jk0kj7LtiWCbJaj4owPFQ7CqpXPAGX1RDCHblfW9t8cNOCNRFng3o",
138
+ "instanceUrl": "https://example-dev-ed.trailblaze.my.salesforce.com",
139
+ "username": "user@example.com",
140
+ "clientId": "PlatformCLI",
141
+ "connectedStatus": "Connected",
142
+ "sfdxAuthUrl": "force://PlatformCLI::nwAeSuiRqvRHrkbMmCKvLHasS0vRbh3Cf2RF41WZzmjtThnCwOuDvn9HObcUjKuTExJPqPegIwnLB5aH6GNWYhU@example-dev-ed.trailblaze.my.salesforce.com",
143
+ "alias": "int"
144
+ }
145
+ }
146
+ ```
147
+
148
+ 4. **Extract and Use the Tokens**:
149
+ The `sfdxAuthUrl` is structured as:
150
+
151
+ ```
152
+ force://<client_id>:<client_secret>:<refresh_token>@<instance_url>
153
+ ```
154
+
155
+ This means with the above output sample, you would use the following information:
156
+
157
+ ```python
158
+ # This is for illustrative purposes; use environment variables instead
159
+ client_id = "PlatformCLI"
160
+ client_secret = ""
161
+ refresh_token = "nwAeSuiRqvRHrkbMmCKvLHasS0vRbh3Cf2RF41WZzmjtThnCwOuDvn9HObcUjKuTExJPqPegIwnLB5aH6GNWYhU"
162
+ instance_url = "https://example-dev-ed.trailblaze.my.salesforce.com"
163
+
164
+ from sfq import SFAuth
165
+ sf = SFAuth(
166
+ instance_url=instance_url,
167
+ client_id=client_id,
168
+ client_secret=client_secret,
169
+ refresh_token=refresh_token,
170
+ )
171
+
172
+ ```
173
+
174
+ ## Important Considerations
175
+
176
+ - **Security**: Safeguard your client_id, client_secret, and refresh_token diligently, as they provide access to your Salesforce environment. Avoid sharing or exposing them in unsecured locations.
177
+ - **Efficient Data Retrieval**: The `query` and `cquery` function automatically handles pagination, simplifying record retrieval across large datasets. It's recommended to use the `LIMIT` clause in queries to control the volume of data returned.
178
+ - **Advanced Tooling Queries**: Utilize the `tooling_query` function to access the Salesforce Tooling API. This option is designed for performing complex operations, enhancing your data management capabilities.
179
+
sfq-0.0.14/README.md ADDED
@@ -0,0 +1,163 @@
1
+ # sfq (Salesforce Query)
2
+
3
+ sfq is a lightweight Python wrapper library designed to simplify querying Salesforce, reducing repetitive code for accessing Salesforce data.
4
+
5
+ For more varied workflows, consider using an alternative like [Simple Salesforce](https://simple-salesforce.readthedocs.io/en/stable/). This library was even referenced on the [Salesforce Developers Blog](https://developer.salesforce.com/blogs/2021/09/how-to-automate-data-extraction-from-salesforce-using-python).
6
+
7
+ ## Features
8
+
9
+ - Simplified query execution for Salesforce instances.
10
+ - Integration with Salesforce authentication via refresh tokens.
11
+ - Option to interact with Salesforce Tooling API for more advanced queries.
12
+
13
+ ## Installation
14
+
15
+ You can install the `sfq` library using `pip`:
16
+
17
+ ```bash
18
+ pip install sfq
19
+ ```
20
+
21
+ ## Usage
22
+
23
+ ### Library Querying
24
+
25
+ ```python
26
+ from sfq import SFAuth
27
+
28
+ # Initialize the SFAuth class with authentication details
29
+ sf = SFAuth(
30
+ instance_url="https://example-dev-ed.trailblaze.my.salesforce.com",
31
+ client_id="your-client-id-here",
32
+ client_secret="your-client-secret-here",
33
+ refresh_token="your-refresh-token-here"
34
+ )
35
+
36
+ # Execute a query to fetch account records
37
+ print(sf.query("SELECT Id FROM Account LIMIT 5"))
38
+
39
+ # Execute a query to fetch Tooling API data
40
+ print(sf.tooling_query("SELECT Id, FullName, Metadata FROM SandboxSettings LIMIT 5"))
41
+ ```
42
+
43
+ ### sObject Key Prefixes
44
+
45
+ ```python
46
+ # Key prefix via IDs
47
+ print(sf.get_sobject_prefixes())
48
+ >>> {'0Pp': 'AIApplication', '6S9': 'AIApplicationConfig', '9qd': 'AIInsightAction', '9bq': 'AIInsightFeedback', '0T2': 'AIInsightReason', '9qc': 'AIInsightValue', ...}
49
+
50
+ # Key prefix via names
51
+ print(sf.get_sobject_prefixes(key_type="name"))
52
+ >>> {'AIApplication': '0Pp', 'AIApplicationConfig': '6S9', 'AIInsightAction': '9qd', 'AIInsightFeedback': '9bq', 'AIInsightReason': '0T2', 'AIInsightValue': '9qc', ...}
53
+ ```
54
+
55
+ ### Composite Batch Queries
56
+
57
+ ```python
58
+ multiple_queries = {
59
+ "Recent Users": """
60
+ SELECT Id, Name,CreatedDate
61
+ FROM User
62
+ ORDER BY CreatedDate DESC
63
+ LIMIT 10""",
64
+ "Recent Accounts": "SELECT Id, Name, CreatedDate FROM Account ORDER BY CreatedDate DESC LIMIT 10",
65
+ "Frozen Users": "SELECT Id, UserId FROM UserLogin WHERE IsFrozen = true", # If exceeds 2000 records, will paginate
66
+ }
67
+
68
+ batched_response = sf.cquery(multiple_queries)
69
+
70
+ for subrequest_identifer, subrequest_response in batched_response.items():
71
+ print(f'"{subrequest_identifer}" returned {subrequest_response["totalSize"]} records')
72
+ >>> "Recent Users" returned 10 records
73
+ >>> "Recent Accounts" returned 10 records
74
+ >>> "Frozen Users" returned 4082 records
75
+ ```
76
+
77
+ ### Static Resources
78
+
79
+ ```python
80
+ page = sf.read_static_resource_id('081aj000009jUMXAA2')
81
+ print(f'Initial resource: {page}')
82
+ >>> Initial resource: <h1>It works!</h1>
83
+ sf.update_static_resource_name('HelloWorld', '<h1>Hello World</h1>')
84
+ page = sf.read_static_resource_name('HelloWorld')
85
+ print(f'Updated resource: {page}')
86
+ >>> Updated resource: <h1>Hello World</h1>
87
+ sf.update_static_resource_id('081aj000009jUMXAA2', '<h1>It works!</h1>')
88
+ ```
89
+
90
+ ## How to Obtain Salesforce Tokens
91
+
92
+ To use the `sfq` library, you'll need a **client ID** and **refresh token**. The easiest way to obtain these is by using the Salesforce CLI:
93
+
94
+ ### Steps to Get Tokens
95
+
96
+ 1. **Install the Salesforce CLI**:
97
+ Follow the instructions on the [Salesforce CLI installation page](https://developer.salesforce.com/tools/salesforcecli).
98
+
99
+ 2. **Authenticate with Salesforce**:
100
+ Login to your Salesforce org using the following command:
101
+
102
+ ```bash
103
+ sf org login web --alias int --instance-url https://corpa--int.sandbox.my.salesforce.com
104
+ ```
105
+
106
+ 3. **Display Org Details**:
107
+ To get the client ID, client secret, refresh token, and instance URL, run:
108
+
109
+ ```bash
110
+ sf org display --target-org int --verbose --json
111
+ ```
112
+
113
+ The output will look like this:
114
+
115
+ ```json
116
+ {
117
+ "status": 0,
118
+ "result": {
119
+ "id": "00Daa0000000000000",
120
+ "apiVersion": "63.0",
121
+ "accessToken": "00Daa0000000000000!evaU3fYZEWGUrqI5rMtaS8KYbHfeqA7YWzMgKToOB43Jk0kj7LtiWCbJaj4owPFQ7CqpXPAGX1RDCHblfW9t8cNOCNRFng3o",
122
+ "instanceUrl": "https://example-dev-ed.trailblaze.my.salesforce.com",
123
+ "username": "user@example.com",
124
+ "clientId": "PlatformCLI",
125
+ "connectedStatus": "Connected",
126
+ "sfdxAuthUrl": "force://PlatformCLI::nwAeSuiRqvRHrkbMmCKvLHasS0vRbh3Cf2RF41WZzmjtThnCwOuDvn9HObcUjKuTExJPqPegIwnLB5aH6GNWYhU@example-dev-ed.trailblaze.my.salesforce.com",
127
+ "alias": "int"
128
+ }
129
+ }
130
+ ```
131
+
132
+ 4. **Extract and Use the Tokens**:
133
+ The `sfdxAuthUrl` is structured as:
134
+
135
+ ```
136
+ force://<client_id>:<client_secret>:<refresh_token>@<instance_url>
137
+ ```
138
+
139
+ This means with the above output sample, you would use the following information:
140
+
141
+ ```python
142
+ # This is for illustrative purposes; use environment variables instead
143
+ client_id = "PlatformCLI"
144
+ client_secret = ""
145
+ refresh_token = "nwAeSuiRqvRHrkbMmCKvLHasS0vRbh3Cf2RF41WZzmjtThnCwOuDvn9HObcUjKuTExJPqPegIwnLB5aH6GNWYhU"
146
+ instance_url = "https://example-dev-ed.trailblaze.my.salesforce.com"
147
+
148
+ from sfq import SFAuth
149
+ sf = SFAuth(
150
+ instance_url=instance_url,
151
+ client_id=client_id,
152
+ client_secret=client_secret,
153
+ refresh_token=refresh_token,
154
+ )
155
+
156
+ ```
157
+
158
+ ## Important Considerations
159
+
160
+ - **Security**: Safeguard your client_id, client_secret, and refresh_token diligently, as they provide access to your Salesforce environment. Avoid sharing or exposing them in unsecured locations.
161
+ - **Efficient Data Retrieval**: The `query` and `cquery` function automatically handles pagination, simplifying record retrieval across large datasets. It's recommended to use the `LIMIT` clause in queries to control the volume of data returned.
162
+ - **Advanced Tooling Queries**: Utilize the `tooling_query` function to access the Salesforce Tooling API. This option is designed for performing complex operations, enhancing your data management capabilities.
163
+
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "sfq"
3
- version = "0.0.12"
3
+ version = "0.0.14"
4
4
  description = "Python wrapper for the Salesforce's Query API."
5
5
  readme = "README.md"
6
6
  authors = [{ name = "David Moruzzi", email = "sfq.pypi@dmoruzi.com" }]
@@ -8,7 +8,7 @@ import warnings
8
8
  from collections import OrderedDict
9
9
  from concurrent.futures import ThreadPoolExecutor, as_completed
10
10
  from queue import Empty, Queue
11
- from typing import Any, Dict, Optional
11
+ from typing import Any, Dict, Literal, Optional
12
12
  from urllib.parse import quote, urlparse
13
13
 
14
14
  TRACE = 5
@@ -81,13 +81,15 @@ class SFAuth:
81
81
  self,
82
82
  instance_url: str,
83
83
  client_id: str,
84
- refresh_token: str,
84
+ refresh_token: str, # client_secret & refresh_token will swap positions 2025-AUG-1
85
+ client_secret: str = "_deprecation_warning", # mandatory after 2025-AUG-1
85
86
  api_version: str = "v63.0",
86
87
  token_endpoint: str = "/services/oauth2/token",
87
88
  access_token: Optional[str] = None,
88
89
  token_expiration_time: Optional[float] = None,
89
90
  token_lifetime: int = 15 * 60,
90
- user_agent: str = "sfq/0.0.12",
91
+ user_agent: str = "sfq/0.0.14",
92
+ sforce_client: str = '_auto',
91
93
  proxy: str = "auto",
92
94
  ) -> None:
93
95
  """
@@ -96,16 +98,19 @@ class SFAuth:
96
98
  :param instance_url: The Salesforce instance URL.
97
99
  :param client_id: The client ID for OAuth.
98
100
  :param refresh_token: The refresh token for OAuth.
101
+ :param client_secret: The client secret for OAuth (default is "_deprecation_warning").
99
102
  :param api_version: The Salesforce API version (default is "v63.0").
100
103
  :param token_endpoint: The token endpoint (default is "/services/oauth2/token").
101
104
  :param access_token: The access token for the current session (default is None).
102
105
  :param token_expiration_time: The expiration time of the access token (default is None).
103
106
  :param token_lifetime: The lifetime of the access token in seconds (default is 15 minutes).
104
- :param user_agent: Custom User-Agent string (default is "sfq/0.0.12").
107
+ :param user_agent: Custom User-Agent string (default is "sfq/0.0.14").
108
+ :param sforce_client: Custom Application Identifier (default is user_agent).
105
109
  :param proxy: The proxy configuration, "auto" to use environment (default is "auto").
106
110
  """
107
- self.instance_url = instance_url
111
+ self.instance_url = self._format_instance_url(instance_url)
108
112
  self.client_id = client_id
113
+ self.client_secret = client_secret
109
114
  self.refresh_token = refresh_token
110
115
  self.api_version = api_version
111
116
  self.token_endpoint = token_endpoint
@@ -113,9 +118,33 @@ class SFAuth:
113
118
  self.token_expiration_time = token_expiration_time
114
119
  self.token_lifetime = token_lifetime
115
120
  self.user_agent = user_agent
121
+ self.sforce_client = sforce_client
116
122
  self._auto_configure_proxy(proxy)
117
123
  self._high_api_usage_threshold = 80
118
124
 
125
+ if sforce_client == '_auto':
126
+ self.sforce_client = user_agent
127
+
128
+ if self.client_secret == "_deprecation_warning":
129
+ warnings.warn(
130
+ "The 'client_secret' parameter will be mandatory and positional arguments will change after 1 August 2025. "
131
+ "Please ensure explicit argument assignment and 'client_secret' inclusion when initializing the SFAuth object.",
132
+ DeprecationWarning,
133
+ stacklevel=2,
134
+ )
135
+
136
+ logger.debug(
137
+ "Will be SFAuth(instance_url, client_id, client_secret, refresh_token) starting 1 August 2025... but please just use named arguments.."
138
+ )
139
+
140
+ def _format_instance_url(self, instance_url) -> str:
141
+ # check if it begins with https://
142
+ if instance_url.startswith("https://"):
143
+ return instance_url
144
+ if instance_url.startswith("http://"):
145
+ return instance_url.replace("http://", "https://")
146
+ return f"https://{instance_url}"
147
+
119
148
  def _auto_configure_proxy(self, proxy: str) -> None:
120
149
  """
121
150
  Automatically configure the proxy based on the environment or provided value.
@@ -128,16 +157,36 @@ class SFAuth:
128
157
  self.proxy = proxy
129
158
  logger.debug("Using configured proxy: %s", self.proxy)
130
159
 
131
- def _prepare_payload(self) -> Dict[str, str]:
160
+ def _prepare_payload(self) -> Dict[str, Optional[str]]:
132
161
  """
133
162
  Prepare the payload for the token request.
163
+
164
+ This method constructs a dictionary containing the necessary parameters
165
+ for a token request using the refresh token grant type. It includes
166
+ the client ID, client secret, and refresh token if they are available.
167
+
168
+ Returns:
169
+ Dict[str, Optional[str]]: A dictionary containing the payload for the token request.
134
170
  """
135
- return {
171
+ payload = {
136
172
  "grant_type": "refresh_token",
137
173
  "client_id": self.client_id,
174
+ "client_secret": self.client_secret,
138
175
  "refresh_token": self.refresh_token,
139
176
  }
140
177
 
178
+ if self.client_secret == "_deprecation_warning":
179
+ logger.warning(
180
+ "The SFQ library is making a breaking change (2025-AUG-1) to require the 'client_secret' parameter to be assigned when initializing the SFAuth object. "
181
+ "In addition, positional arguments will change. Please ensure explicit argument assignment and 'client_secret' inclusion when initializing the SFAuth object to avoid impact."
182
+ )
183
+ payload.pop("client_secret")
184
+
185
+ if not self.client_secret:
186
+ payload.pop("client_secret")
187
+
188
+ return payload
189
+
141
190
  def _create_connection(self, netloc: str) -> http.client.HTTPConnection:
142
191
  """
143
192
  Create a connection using HTTP or HTTPS, with optional proxy support.
@@ -169,6 +218,7 @@ class SFAuth:
169
218
  "Accept": "application/json",
170
219
  "Content-Type": "application/x-www-form-urlencoded",
171
220
  "User-Agent": self.user_agent,
221
+ "Sforce-Call-Options": f"client={self.sforce_client}",
172
222
  }
173
223
  body = "&".join(f"{key}={quote(str(value))}" for key, value in payload.items())
174
224
 
@@ -214,27 +264,24 @@ class SFAuth:
214
264
  headers_list = [(k, v) for k, v in headers if not v.startswith("BrowserId=")]
215
265
  logger.trace("Response headers: %s", headers_list)
216
266
  for key, value in headers_list:
217
- if key.startswith("Sforce-"):
218
- if key == "Sforce-Limit-Info":
219
- current_api_calls = int(value.split("=")[1].split("/")[0])
220
- maximum_api_calls = int(value.split("=")[1].split("/")[1])
221
- usage_percentage = round(
222
- current_api_calls / maximum_api_calls * 100, 2
267
+ if key == "Sforce-Limit-Info":
268
+ current_api_calls = int(value.split("=")[1].split("/")[0])
269
+ maximum_api_calls = int(value.split("=")[1].split("/")[1])
270
+ usage_percentage = round(current_api_calls / maximum_api_calls * 100, 2)
271
+ if usage_percentage > self._high_api_usage_threshold:
272
+ logger.warning(
273
+ "High API usage: %s/%s (%s%%)",
274
+ current_api_calls,
275
+ maximum_api_calls,
276
+ usage_percentage,
277
+ )
278
+ else:
279
+ logger.debug(
280
+ "API usage: %s/%s (%s%%)",
281
+ current_api_calls,
282
+ maximum_api_calls,
283
+ usage_percentage,
223
284
  )
224
- if usage_percentage > self._high_api_usage_threshold:
225
- logger.warning(
226
- "High API usage: %s/%s (%s%%)",
227
- current_api_calls,
228
- maximum_api_calls,
229
- usage_percentage,
230
- )
231
- else:
232
- logger.debug(
233
- "API usage: %s/%s (%s%%)",
234
- current_api_calls,
235
- maximum_api_calls,
236
- usage_percentage,
237
- )
238
285
 
239
286
  def _refresh_token_if_needed(self) -> Optional[str]:
240
287
  """
@@ -298,6 +345,7 @@ class SFAuth:
298
345
  _safe_resource_name = quote(resource_name, safe="")
299
346
  query = f"SELECT Id FROM StaticResource WHERE Name = '{_safe_resource_name}'"
300
347
  if namespace:
348
+ namespace = quote(namespace, safe="")
301
349
  query += f" AND NamespacePrefix = '{namespace}'"
302
350
  query += " LIMIT 1"
303
351
  _static_resource_id_response = self.query(query)
@@ -331,6 +379,7 @@ class SFAuth:
331
379
  headers = {
332
380
  "Authorization": f"Bearer {self.access_token}",
333
381
  "User-Agent": self.user_agent,
382
+ "Sforce-Call-Options": f"client={self.sforce_client}",
334
383
  "Accept": "application/json",
335
384
  }
336
385
 
@@ -382,6 +431,7 @@ class SFAuth:
382
431
  safe_resource_name = quote(resource_name, safe="")
383
432
  query = f"SELECT Id FROM StaticResource WHERE Name = '{safe_resource_name}'"
384
433
  if namespace:
434
+ namespace = quote(namespace, safe="")
385
435
  query += f" AND NamespacePrefix = '{namespace}'"
386
436
  query += " LIMIT 1"
387
437
 
@@ -425,6 +475,7 @@ class SFAuth:
425
475
  headers = {
426
476
  "Authorization": f"Bearer {self.access_token}",
427
477
  "User-Agent": self.user_agent,
478
+ "Sforce-Call-Options": f"client={self.sforce_client}",
428
479
  "Content-Type": "application/json",
429
480
  "Accept": "application/json",
430
481
  }
@@ -483,6 +534,7 @@ class SFAuth:
483
534
  headers = {
484
535
  "Authorization": f"Bearer {self.access_token}",
485
536
  "User-Agent": self.user_agent,
537
+ "Sforce-Call-Options": f"client={self.sforce_client}",
486
538
  "Accept": "application/json",
487
539
  }
488
540
 
@@ -539,6 +591,7 @@ class SFAuth:
539
591
  headers = {
540
592
  "Authorization": f"Bearer {self.access_token}",
541
593
  "User-Agent": self.user_agent,
594
+ "Sforce-Call-Options": f"client={self.sforce_client}",
542
595
  "Accept": "application/json",
543
596
  }
544
597
 
@@ -607,7 +660,86 @@ class SFAuth:
607
660
  """
608
661
  return self.query(query, tooling=True)
609
662
 
610
- def cquery(self, query_dict: dict[str, str], max_workers: int = 10) -> Optional[Dict[str, Any]]:
663
+ def get_sobject_prefixes(
664
+ self, key_type: Literal["id", "name"] = "id"
665
+ ) -> Optional[Dict[str, str]]:
666
+ """
667
+ Fetch all key prefixes from the Salesforce instance and map them to sObject names or vice versa.
668
+
669
+ :param key_type: The type of key to return. Either 'id' (prefix) or 'name' (sObject).
670
+ :return: A dictionary mapping key prefixes to sObject names or None on failure.
671
+ """
672
+ valid_key_types = {"id", "name"}
673
+ if key_type not in valid_key_types:
674
+ logger.error(
675
+ "Invalid key type: %s, must be one of: %s",
676
+ key_type,
677
+ ", ".join(valid_key_types),
678
+ )
679
+ return None
680
+
681
+ self._refresh_token_if_needed()
682
+
683
+ if not self.access_token:
684
+ logger.error("No access token available for key prefixes.")
685
+ return None
686
+
687
+ endpoint = f"/services/data/{self.api_version}/sobjects/"
688
+ headers = {
689
+ "Authorization": f"Bearer {self.access_token}",
690
+ "User-Agent": self.user_agent,
691
+ "Sforce-Call-Options": f"client={self.sforce_client}",
692
+ "Accept": "application/json",
693
+ }
694
+
695
+ parsed_url = urlparse(self.instance_url)
696
+ conn = self._create_connection(parsed_url.netloc)
697
+ prefixes = {}
698
+
699
+ try:
700
+ logger.trace("Request endpoint: %s", endpoint)
701
+ logger.trace("Request headers: %s", headers)
702
+ conn.request("GET", endpoint, headers=headers)
703
+ response = conn.getresponse()
704
+ data = response.read().decode("utf-8")
705
+ self._http_resp_header_logic(response)
706
+
707
+ if response.status == 200:
708
+ logger.debug("Key prefixes API request successful.")
709
+ logger.trace("Response body: %s", data)
710
+ for sobject in json.loads(data)["sobjects"]:
711
+ key_prefix = sobject.get("keyPrefix")
712
+ name = sobject.get("name")
713
+ if not key_prefix or not name:
714
+ continue
715
+
716
+ if key_type == "id":
717
+ prefixes[key_prefix] = name
718
+ elif key_type == "name":
719
+ prefixes[name] = key_prefix
720
+
721
+ logger.debug("Key prefixes: %s", prefixes)
722
+ return prefixes
723
+
724
+ logger.error(
725
+ "Key prefixes API request failed: %s %s",
726
+ response.status,
727
+ response.reason,
728
+ )
729
+ logger.debug("Response body: %s", data)
730
+
731
+ except Exception as err:
732
+ logger.exception("Exception during key prefixes API request: %s", err)
733
+
734
+ finally:
735
+ logger.trace("Closing connection...")
736
+ conn.close()
737
+
738
+ return None
739
+
740
+ def cquery(
741
+ self, query_dict: dict[str, str], max_workers: int = 10
742
+ ) -> Optional[Dict[str, Any]]:
611
743
  """
612
744
  Execute multiple SOQL queries using the Composite Batch API with threading to reduce network overhead.
613
745
  The function returns a dictionary mapping the original keys to their corresponding batch response.
@@ -633,6 +765,7 @@ class SFAuth:
633
765
  headers = {
634
766
  "Authorization": f"Bearer {self.access_token}",
635
767
  "User-Agent": self.user_agent,
768
+ "Sforce-Call-Options": f"client={self.sforce_client}",
636
769
  "Accept": "application/json",
637
770
  "Content-Type": "application/json",
638
771
  }
@@ -668,7 +801,34 @@ class SFAuth:
668
801
  logger.trace("Composite query full response: %s", data)
669
802
  results = json.loads(data).get("results", [])
670
803
  for i, result in enumerate(results):
671
- batch_results[keys[i]] = result
804
+ records = []
805
+ if "result" in result and "records" in result["result"]:
806
+ records.extend(result["result"]["records"])
807
+ # Handle pagination
808
+ while not result["result"].get("done", True):
809
+ next_url = result["result"].get("nextRecordsUrl")
810
+ if next_url:
811
+ conn.request("GET", next_url, headers=headers)
812
+ response = conn.getresponse()
813
+ data = response.read().decode("utf-8")
814
+ self._http_resp_header_logic(response)
815
+ if response.status == 200:
816
+ next_results = json.loads(data)
817
+ records.extend(next_results.get("records", []))
818
+ result["result"]["done"] = next_results.get("done")
819
+ else:
820
+ logger.error(
821
+ "Failed to fetch next records: %s",
822
+ response.reason,
823
+ )
824
+ break
825
+ else:
826
+ result["result"]["done"] = True
827
+ paginated_results = result["result"]
828
+ paginated_results["records"] = records
829
+ if "nextRecordsUrl" in paginated_results:
830
+ del paginated_results["nextRecordsUrl"]
831
+ batch_results[keys[i]] = paginated_results
672
832
  if result.get("statusCode") != 200:
673
833
  logger.error("Query failed for key %s: %s", keys[i], result)
674
834
  logger.error(
@@ -709,6 +869,13 @@ class SFAuth:
709
869
  logger.trace("Composite query results: %s", results_dict)
710
870
  return results_dict
711
871
 
872
+ def _reconnect_with_backoff(self, attempt: int) -> None:
873
+ wait_time = min(2**attempt, 60)
874
+ logger.warning(
875
+ f"Reconnecting after failure, backoff {wait_time}s (attempt {attempt})"
876
+ )
877
+ time.sleep(wait_time)
878
+
712
879
  def _subscribe_topic(
713
880
  self,
714
881
  topic: str,
@@ -742,6 +909,7 @@ class SFAuth:
742
909
  "Content-Type": "application/json",
743
910
  "Accept": "application/json",
744
911
  "User-Agent": self.user_agent,
912
+ "Sforce-Call-Options": f"client={self.sforce_client}",
745
913
  }
746
914
 
747
915
  parsed_url = urlparse(self.instance_url)
@@ -3,5 +3,5 @@ requires-python = ">=3.9"
3
3
 
4
4
  [[package]]
5
5
  name = "sfq"
6
- version = "0.0.12"
6
+ version = "0.0.14"
7
7
  source = { editable = "." }
sfq-0.0.12/PKG-INFO DELETED
@@ -1,133 +0,0 @@
1
- Metadata-Version: 2.4
2
- Name: sfq
3
- Version: 0.0.12
4
- Summary: Python wrapper for the Salesforce's Query API.
5
- Author-email: David Moruzzi <sfq.pypi@dmoruzi.com>
6
- Keywords: salesforce,salesforce query
7
- Classifier: Development Status :: 3 - Alpha
8
- Classifier: Intended Audience :: Developers
9
- Classifier: Programming Language :: Python :: 3.9
10
- Classifier: Programming Language :: Python :: 3.10
11
- Classifier: Programming Language :: Python :: 3.12
12
- Classifier: Programming Language :: Python :: 3.13
13
- Classifier: Topic :: Software Development :: Libraries :: Python Modules
14
- Requires-Python: >=3.9
15
- Description-Content-Type: text/markdown
16
-
17
- # sfq (Salesforce Query)
18
-
19
- sfq is a lightweight Python wrapper library designed to simplify querying Salesforce, reducing repetitive code for accessing Salesforce data.
20
-
21
- For more varied workflows, consider using an alternative like [Simple Salesforce](https://simple-salesforce.readthedocs.io/en/stable/). This library was even referenced on the [Salesforce Developers Blog](https://developer.salesforce.com/blogs/2021/09/how-to-automate-data-extraction-from-salesforce-using-python).
22
-
23
- ## Features
24
-
25
- - Simplified query execution for Salesforce instances.
26
- - Integration with Salesforce authentication via refresh tokens.
27
- - Option to interact with Salesforce Tooling API for more advanced queries.
28
-
29
- ## Installation
30
-
31
- You can install the `sfq` library using `pip`:
32
-
33
- ```bash
34
- pip install sfq
35
- ```
36
-
37
- ## Usage
38
-
39
- ### Library Querying
40
-
41
- ```python
42
- from sfq import SFAuth
43
-
44
- # Initialize the SFAuth class with authentication details
45
- sf = SFAuth(
46
- instance_url="https://example-dev-ed.trailblaze.my.salesforce.com",
47
- client_id="PlatformCLI",
48
- refresh_token="your-refresh-token-here"
49
- )
50
-
51
- # Execute a query to fetch account records
52
- print(sf.query("SELECT Id FROM Account LIMIT 5"))
53
-
54
- # Execute a query to fetch Tooling API data
55
- print(sf.query("SELECT Id, FullName, Metadata FROM SandboxSettings LIMIT 5", tooling=True))
56
- ```
57
-
58
- ### Bash Querying
59
-
60
- You can easily incorporate this into ad-hoc bash scripts or commands:
61
-
62
- ```bash
63
- python -c "from sfq import SFAuth; sf = SFAuth(instance_url='$instance_url', client_id='$client_id', refresh_token='$refresh_token'); print(sf.query('$query'))" | jq -r '.records[].Id'
64
- ```
65
-
66
- ## How to Obtain Salesforce Tokens
67
-
68
- To use the `sfq` library, you'll need a **client ID** and **refresh token**. The easiest way to obtain these is by using the Salesforce CLI:
69
-
70
- ### Steps to Get Tokens
71
-
72
- 1. **Install the Salesforce CLI**:
73
- Follow the instructions on the [Salesforce CLI installation page](https://developer.salesforce.com/tools/salesforcecli).
74
-
75
- 2. **Authenticate with Salesforce**:
76
- Login to your Salesforce org using the following command:
77
-
78
- ```bash
79
- sf org login web --alias int --instance-url https://corpa--int.sandbox.my.salesforce.com
80
- ```
81
-
82
- 3. **Display Org Details**:
83
- To get the client ID, refresh token, and instance URL, run:
84
-
85
- ```bash
86
- sf org display --target-org int --verbose --json
87
- ```
88
-
89
- The output will look like this:
90
-
91
- ```json
92
- {
93
- "status": 0,
94
- "result": {
95
- "id": "00Daa0000000000000",
96
- "apiVersion": "63.0",
97
- "accessToken": "your-access-token-here",
98
- "instanceUrl": "https://example-dev-ed.trailblaze.my.salesforce.com",
99
- "username": "user@example.com",
100
- "clientId": "PlatformCLI",
101
- "connectedStatus": "Connected",
102
- "sfdxAuthUrl": "force://PlatformCLI::your-refresh-token-here::https://example-dev-ed.trailblaze.my.salesforce.com",
103
- "alias": "int"
104
- }
105
- }
106
- ```
107
-
108
- 4. **Extract and Use the Tokens**:
109
- The `sfdxAuthUrl` is structured as:
110
-
111
- ```
112
- force://<client_id>::<refresh_token>::<instance_url>
113
- ```
114
-
115
- You can extract and use the tokens in a bash script like this:
116
-
117
- ```bash
118
- query="SELECT Id FROM User WHERE IsActive = true AND Profile.Name = 'System Administrator'"
119
-
120
- sfdxAuthUrl=$(sf org display --target-org int --verbose --json | jq -r '.result.sfdxAuthUrl' | sed 's/force:\/\///')
121
- clientId=$(echo "$sfdxAuthUrl" | sed 's/::/\n/g' | sed -n '1p')
122
- refreshToken=$(echo "$sfdxAuthUrl" | sed 's/::/\n/g' | sed -n '2p')
123
- instanceUrl=$(echo "$sfdxAuthUrl" | sed 's/::/\n/g' | sed -n '3p')
124
-
125
- pip install sfq && python -c "from sfq import SFAuth; sf = SFAuth(instance_url='$instanceUrl', client_id='$clientId', refresh_token='$refreshToken'); print(sf.query('$query'))" | jq -r '.records[].Id'
126
- ```
127
-
128
- ## Important Considerations
129
-
130
- - **Security**: Safeguard your refresh token diligently, as it provides access to your Salesforce environment. Avoid sharing or exposing it in unsecured locations.
131
- - **Efficient Data Retrieval**: The `query` function automatically handles pagination, simplifying record retrieval across large datasets. It's recommended to use the `LIMIT` clause in queries to control the volume of data returned.
132
- - **Advanced Metadata Queries**: Utilize the `tooling=True` option within the `query` function to access the Salesforce Tooling API. This option is designed for performing complex metadata operations, enhancing your data management capabilities.
133
-
sfq-0.0.12/README.md DELETED
@@ -1,117 +0,0 @@
1
- # sfq (Salesforce Query)
2
-
3
- sfq is a lightweight Python wrapper library designed to simplify querying Salesforce, reducing repetitive code for accessing Salesforce data.
4
-
5
- For more varied workflows, consider using an alternative like [Simple Salesforce](https://simple-salesforce.readthedocs.io/en/stable/). This library was even referenced on the [Salesforce Developers Blog](https://developer.salesforce.com/blogs/2021/09/how-to-automate-data-extraction-from-salesforce-using-python).
6
-
7
- ## Features
8
-
9
- - Simplified query execution for Salesforce instances.
10
- - Integration with Salesforce authentication via refresh tokens.
11
- - Option to interact with Salesforce Tooling API for more advanced queries.
12
-
13
- ## Installation
14
-
15
- You can install the `sfq` library using `pip`:
16
-
17
- ```bash
18
- pip install sfq
19
- ```
20
-
21
- ## Usage
22
-
23
- ### Library Querying
24
-
25
- ```python
26
- from sfq import SFAuth
27
-
28
- # Initialize the SFAuth class with authentication details
29
- sf = SFAuth(
30
- instance_url="https://example-dev-ed.trailblaze.my.salesforce.com",
31
- client_id="PlatformCLI",
32
- refresh_token="your-refresh-token-here"
33
- )
34
-
35
- # Execute a query to fetch account records
36
- print(sf.query("SELECT Id FROM Account LIMIT 5"))
37
-
38
- # Execute a query to fetch Tooling API data
39
- print(sf.query("SELECT Id, FullName, Metadata FROM SandboxSettings LIMIT 5", tooling=True))
40
- ```
41
-
42
- ### Bash Querying
43
-
44
- You can easily incorporate this into ad-hoc bash scripts or commands:
45
-
46
- ```bash
47
- python -c "from sfq import SFAuth; sf = SFAuth(instance_url='$instance_url', client_id='$client_id', refresh_token='$refresh_token'); print(sf.query('$query'))" | jq -r '.records[].Id'
48
- ```
49
-
50
- ## How to Obtain Salesforce Tokens
51
-
52
- To use the `sfq` library, you'll need a **client ID** and **refresh token**. The easiest way to obtain these is by using the Salesforce CLI:
53
-
54
- ### Steps to Get Tokens
55
-
56
- 1. **Install the Salesforce CLI**:
57
- Follow the instructions on the [Salesforce CLI installation page](https://developer.salesforce.com/tools/salesforcecli).
58
-
59
- 2. **Authenticate with Salesforce**:
60
- Login to your Salesforce org using the following command:
61
-
62
- ```bash
63
- sf org login web --alias int --instance-url https://corpa--int.sandbox.my.salesforce.com
64
- ```
65
-
66
- 3. **Display Org Details**:
67
- To get the client ID, refresh token, and instance URL, run:
68
-
69
- ```bash
70
- sf org display --target-org int --verbose --json
71
- ```
72
-
73
- The output will look like this:
74
-
75
- ```json
76
- {
77
- "status": 0,
78
- "result": {
79
- "id": "00Daa0000000000000",
80
- "apiVersion": "63.0",
81
- "accessToken": "your-access-token-here",
82
- "instanceUrl": "https://example-dev-ed.trailblaze.my.salesforce.com",
83
- "username": "user@example.com",
84
- "clientId": "PlatformCLI",
85
- "connectedStatus": "Connected",
86
- "sfdxAuthUrl": "force://PlatformCLI::your-refresh-token-here::https://example-dev-ed.trailblaze.my.salesforce.com",
87
- "alias": "int"
88
- }
89
- }
90
- ```
91
-
92
- 4. **Extract and Use the Tokens**:
93
- The `sfdxAuthUrl` is structured as:
94
-
95
- ```
96
- force://<client_id>::<refresh_token>::<instance_url>
97
- ```
98
-
99
- You can extract and use the tokens in a bash script like this:
100
-
101
- ```bash
102
- query="SELECT Id FROM User WHERE IsActive = true AND Profile.Name = 'System Administrator'"
103
-
104
- sfdxAuthUrl=$(sf org display --target-org int --verbose --json | jq -r '.result.sfdxAuthUrl' | sed 's/force:\/\///')
105
- clientId=$(echo "$sfdxAuthUrl" | sed 's/::/\n/g' | sed -n '1p')
106
- refreshToken=$(echo "$sfdxAuthUrl" | sed 's/::/\n/g' | sed -n '2p')
107
- instanceUrl=$(echo "$sfdxAuthUrl" | sed 's/::/\n/g' | sed -n '3p')
108
-
109
- pip install sfq && python -c "from sfq import SFAuth; sf = SFAuth(instance_url='$instanceUrl', client_id='$clientId', refresh_token='$refreshToken'); print(sf.query('$query'))" | jq -r '.records[].Id'
110
- ```
111
-
112
- ## Important Considerations
113
-
114
- - **Security**: Safeguard your refresh token diligently, as it provides access to your Salesforce environment. Avoid sharing or exposing it in unsecured locations.
115
- - **Efficient Data Retrieval**: The `query` function automatically handles pagination, simplifying record retrieval across large datasets. It's recommended to use the `LIMIT` clause in queries to control the volume of data returned.
116
- - **Advanced Metadata Queries**: Utilize the `tooling=True` option within the `query` function to access the Salesforce Tooling API. This option is designed for performing complex metadata operations, enhancing your data management capabilities.
117
-
File without changes
File without changes
File without changes
File without changes