sfq 0.0.32__tar.gz → 0.0.34__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.32 → sfq-0.0.34}/.github/workflows/publish.yml +7 -1
- {sfq-0.0.32 → sfq-0.0.34}/PKG-INFO +1 -1
- {sfq-0.0.32 → sfq-0.0.34}/pyproject.toml +1 -1
- sfq-0.0.34/src/sfq/__init__.py +563 -0
- {sfq-0.0.32 → sfq-0.0.34}/src/sfq/_cometd.py +7 -10
- sfq-0.0.34/src/sfq/auth.py +401 -0
- sfq-0.0.34/src/sfq/crud.py +514 -0
- sfq-0.0.34/src/sfq/debug_cleanup.py +71 -0
- sfq-0.0.34/src/sfq/exceptions.py +54 -0
- sfq-0.0.34/src/sfq/http_client.py +319 -0
- sfq-0.0.34/src/sfq/query.py +398 -0
- sfq-0.0.34/src/sfq/soap.py +186 -0
- sfq-0.0.34/src/sfq/utils.py +196 -0
- sfq-0.0.34/tests/test_auth.py +894 -0
- {sfq-0.0.32 → sfq-0.0.34}/tests/test_cdelete.py +7 -10
- sfq-0.0.34/tests/test_compatibility.py +668 -0
- {sfq-0.0.32 → sfq-0.0.34}/tests/test_cquery.py +16 -12
- {sfq-0.0.32 → sfq-0.0.34}/tests/test_create.py +4 -11
- sfq-0.0.34/tests/test_crud.py +640 -0
- sfq-0.0.34/tests/test_crud_e2e.py +606 -0
- {sfq-0.0.32 → sfq-0.0.34}/tests/test_cupdate.py +60 -28
- sfq-0.0.32/tests/test_debug_cleanup.py → sfq-0.0.34/tests/test_debug_cleanup_e2e.py +47 -14
- sfq-0.0.34/tests/test_debug_cleanup_unit.py +57 -0
- sfq-0.0.34/tests/test_http_client.py +513 -0
- {sfq-0.0.32 → sfq-0.0.34}/tests/test_limits_api.py +2 -8
- {sfq-0.0.32 → sfq-0.0.34}/tests/test_log_trace_redact.py +17 -11
- {sfq-0.0.32 → sfq-0.0.34}/tests/test_open_frontdoor.py +2 -8
- sfq-0.0.34/tests/test_query.py +67 -0
- sfq-0.0.34/tests/test_query_client.py +887 -0
- sfq-0.0.34/tests/test_query_e2e.py +363 -0
- sfq-0.0.34/tests/test_query_integration.py +277 -0
- sfq-0.0.34/tests/test_soap.py +390 -0
- sfq-0.0.34/tests/test_soap_batch_operation.py +42 -0
- sfq-0.0.34/tests/test_static_resources.py +100 -0
- sfq-0.0.34/tests/test_utils.py +229 -0
- {sfq-0.0.32 → sfq-0.0.34}/uv.lock +1 -1
- sfq-0.0.32/src/sfq/__init__.py +0 -1237
- sfq-0.0.32/tests/test_query.py +0 -126
- {sfq-0.0.32 → sfq-0.0.34}/.gitignore +0 -0
- {sfq-0.0.32 → sfq-0.0.34}/.python-version +0 -0
- {sfq-0.0.32 → sfq-0.0.34}/README.md +0 -0
- {sfq-0.0.32 → sfq-0.0.34}/src/sfq/py.typed +0 -0
@@ -76,13 +76,17 @@ jobs:
|
|
76
76
|
|
77
77
|
echo "Updating src/sfq/__init__.py user_agent to $VERSION"
|
78
78
|
sed -i -E "s/(user_agent: str = \"sfq\/)[0-9]+\.[0-9]+\.[0-9]+(\")/\1$VERSION\2/" src/sfq/__init__.py
|
79
|
+
sed -i -E "s/(user_agent: str = \"sfq\/)[0-9]+\.[0-9]+\.[0-9]+(\")/\1$VERSION\2/" src/sfq/http_client.py
|
79
80
|
sed -i -E "s/(default is \"sfq\/)[0-9]+\.[0-9]+\.[0-9]+(\")/\1$VERSION\2/" src/sfq/__init__.py
|
80
81
|
|
81
82
|
echo "Updating src/sfq/__init__.py __version__ to $VERSION"
|
82
83
|
sed -i -E "s/(self.__version__ = \")[0-9]+\.[0-9]+\.[0-9]+(\")/\1$VERSION\2/" src/sfq/__init__.py
|
84
|
+
sed -i -E "s/(__version__ = \")[0-9]+\.[0-9]+\.[0-9]+(\")/\1$VERSION\2/" src/sfq/__init__.py
|
85
|
+
|
83
86
|
- name: Run tests
|
84
87
|
run: pytest --verbose --strict-config
|
85
88
|
env:
|
89
|
+
PYTHONPATH: src
|
86
90
|
SF_INSTANCE_URL: ${{ secrets.SF_INSTANCE_URL }}
|
87
91
|
SF_CLIENT_ID: ${{ secrets.SF_CLIENT_ID }}
|
88
92
|
SF_CLIENT_SECRET: ${{ secrets.SF_CLIENT_SECRET }}
|
@@ -122,7 +126,9 @@ jobs:
|
|
122
126
|
run: pip install pdoc
|
123
127
|
|
124
128
|
- name: Generate docs
|
125
|
-
run: pdoc
|
129
|
+
run: pdoc sfq -o docs --no-search
|
130
|
+
env:
|
131
|
+
PYTHONPATH: src
|
126
132
|
|
127
133
|
- name: Set sfq.html as default index page
|
128
134
|
run: |
|
@@ -0,0 +1,563 @@
|
|
1
|
+
"""
|
2
|
+
.. include:: ../../README.md
|
3
|
+
"""
|
4
|
+
|
5
|
+
import webbrowser
|
6
|
+
from typing import Any, Dict, Iterable, List, Literal, Optional
|
7
|
+
from urllib.parse import quote
|
8
|
+
|
9
|
+
# Import new modular components
|
10
|
+
from .auth import AuthManager
|
11
|
+
from .crud import CRUDClient
|
12
|
+
|
13
|
+
# Re-export all public classes and functions for backward compatibility
|
14
|
+
from .exceptions import (
|
15
|
+
APIError,
|
16
|
+
AuthenticationError,
|
17
|
+
ConfigurationError,
|
18
|
+
CRUDError,
|
19
|
+
HTTPError,
|
20
|
+
QueryError,
|
21
|
+
SFQException,
|
22
|
+
SOAPError,
|
23
|
+
)
|
24
|
+
from .http_client import HTTPClient
|
25
|
+
from .query import QueryClient
|
26
|
+
from .soap import SOAPClient
|
27
|
+
from .utils import get_logger
|
28
|
+
from .debug_cleanup import DebugCleanup
|
29
|
+
|
30
|
+
# Define public API for documentation tools
|
31
|
+
__all__ = [
|
32
|
+
"SFAuth",
|
33
|
+
# Exception classes
|
34
|
+
"SFQException",
|
35
|
+
"AuthenticationError",
|
36
|
+
"APIError",
|
37
|
+
"QueryError",
|
38
|
+
"CRUDError",
|
39
|
+
"SOAPError",
|
40
|
+
"HTTPError",
|
41
|
+
"ConfigurationError",
|
42
|
+
# Package metadata
|
43
|
+
"__version__",
|
44
|
+
]
|
45
|
+
|
46
|
+
__version__ = "0.0.34"
|
47
|
+
"""
|
48
|
+
### `__version__`
|
49
|
+
|
50
|
+
**The version of the sfq library.**
|
51
|
+
- Schema: `MAJOR.MINOR.PATCH`
|
52
|
+
- Used for debugging and compatibility checks
|
53
|
+
- Updated to reflect the current library version via CI/CD automation
|
54
|
+
"""
|
55
|
+
logger = get_logger("sfq")
|
56
|
+
|
57
|
+
|
58
|
+
class SFAuth:
|
59
|
+
def __init__(
|
60
|
+
self,
|
61
|
+
instance_url: str,
|
62
|
+
client_id: str,
|
63
|
+
client_secret: str,
|
64
|
+
refresh_token: str,
|
65
|
+
api_version: str = "v64.0",
|
66
|
+
token_endpoint: str = "/services/oauth2/token",
|
67
|
+
access_token: Optional[str] = None,
|
68
|
+
token_expiration_time: Optional[float] = None,
|
69
|
+
token_lifetime: int = 15 * 60,
|
70
|
+
user_agent: str = "sfq/0.0.34",
|
71
|
+
sforce_client: str = "_auto",
|
72
|
+
proxy: str = "_auto",
|
73
|
+
) -> None:
|
74
|
+
"""
|
75
|
+
Initializes the SFAuth with necessary parameters.
|
76
|
+
|
77
|
+
:param instance_url: The Salesforce instance URL.
|
78
|
+
:param client_id: The client ID for OAuth.
|
79
|
+
:param refresh_token: The refresh token for OAuth.
|
80
|
+
:param client_secret: The client secret for OAuth.
|
81
|
+
:param api_version: The Salesforce API version.
|
82
|
+
:param token_endpoint: The token endpoint.
|
83
|
+
:param access_token: The access token for the current session.
|
84
|
+
:param token_expiration_time: The expiration time of the access token.
|
85
|
+
:param token_lifetime: The lifetime of the access token in seconds.
|
86
|
+
:param user_agent: Custom User-Agent string.
|
87
|
+
:param sforce_client: Custom Application Identifier.
|
88
|
+
:param proxy: The proxy configuration, "_auto" to use environment.
|
89
|
+
"""
|
90
|
+
# Initialize the AuthManager with all authentication-related parameters
|
91
|
+
self._auth_manager = AuthManager(
|
92
|
+
instance_url=instance_url,
|
93
|
+
client_id=client_id,
|
94
|
+
refresh_token=refresh_token,
|
95
|
+
client_secret=str(client_secret).strip(),
|
96
|
+
api_version=api_version,
|
97
|
+
token_endpoint=token_endpoint,
|
98
|
+
access_token=access_token,
|
99
|
+
token_expiration_time=token_expiration_time,
|
100
|
+
token_lifetime=token_lifetime,
|
101
|
+
proxy=proxy,
|
102
|
+
)
|
103
|
+
|
104
|
+
# Initialize the HTTPClient with auth manager and user agent settings
|
105
|
+
self._http_client = HTTPClient(
|
106
|
+
auth_manager=self._auth_manager,
|
107
|
+
user_agent=user_agent,
|
108
|
+
sforce_client=sforce_client,
|
109
|
+
high_api_usage_threshold=80,
|
110
|
+
)
|
111
|
+
|
112
|
+
# Initialize the SOAPClient
|
113
|
+
self._soap_client = SOAPClient(
|
114
|
+
http_client=self._http_client,
|
115
|
+
api_version=api_version,
|
116
|
+
)
|
117
|
+
|
118
|
+
# Initialize the QueryClient
|
119
|
+
self._query_client = QueryClient(
|
120
|
+
http_client=self._http_client,
|
121
|
+
api_version=api_version,
|
122
|
+
)
|
123
|
+
|
124
|
+
# Initialize the CRUDClient
|
125
|
+
self._crud_client = CRUDClient(
|
126
|
+
http_client=self._http_client,
|
127
|
+
soap_client=self._soap_client,
|
128
|
+
api_version=api_version,
|
129
|
+
)
|
130
|
+
|
131
|
+
# Initialize the DebugCleanup
|
132
|
+
self._debug_cleanup = DebugCleanup(sf_auth=self)
|
133
|
+
|
134
|
+
# Store version information
|
135
|
+
self.__version__ = "0.0.34"
|
136
|
+
"""
|
137
|
+
### `__version__`
|
138
|
+
|
139
|
+
**The version of the sfq library.**
|
140
|
+
- Schema: `MAJOR.MINOR.PATCH`
|
141
|
+
- Used for debugging and compatibility checks
|
142
|
+
- Updated to reflect the current library version via CI/CD automation
|
143
|
+
"""
|
144
|
+
|
145
|
+
# Property delegation to preserve all existing public attributes
|
146
|
+
@property
|
147
|
+
def instance_url(self) -> str:
|
148
|
+
"""
|
149
|
+
### `instance_url`
|
150
|
+
**The fully qualified Salesforce instance URL.**
|
151
|
+
|
152
|
+
- Should end with `.my.salesforce.com`
|
153
|
+
- No trailing slash
|
154
|
+
|
155
|
+
**Examples:**
|
156
|
+
- `https://sfq-dev-ed.trailblazer.my.salesforce.com`
|
157
|
+
- `https://sfq.my.salesforce.com`
|
158
|
+
- `https://sfq--dev.sandbox.my.salesforce.com`
|
159
|
+
"""
|
160
|
+
return self._auth_manager.instance_url
|
161
|
+
|
162
|
+
@property
|
163
|
+
def client_id(self) -> str:
|
164
|
+
"""
|
165
|
+
### `client_id`
|
166
|
+
**The OAuth client ID.**
|
167
|
+
|
168
|
+
- Uniquely identifies your **Connected App** in Salesforce
|
169
|
+
- If using **Salesforce CLI**, this is `"PlatformCLI"`
|
170
|
+
- For other apps, find this value in the **Connected App details**
|
171
|
+
"""
|
172
|
+
return self._auth_manager.client_id
|
173
|
+
|
174
|
+
@property
|
175
|
+
def client_secret(self) -> str:
|
176
|
+
"""
|
177
|
+
### `client_secret`
|
178
|
+
**The OAuth client secret.**
|
179
|
+
|
180
|
+
- Secret key associated with your Connected App
|
181
|
+
- For **Salesforce CLI**, this is typically an empty string `""`
|
182
|
+
- For custom apps, locate it in the **Connected App settings**
|
183
|
+
"""
|
184
|
+
return self._auth_manager.client_secret
|
185
|
+
|
186
|
+
@property
|
187
|
+
def refresh_token(self) -> str:
|
188
|
+
"""
|
189
|
+
### `refresh_token`
|
190
|
+
**The OAuth refresh token.**
|
191
|
+
|
192
|
+
- Used to fetch new access tokens when the current one expires
|
193
|
+
- For CLI, obtain via:
|
194
|
+
|
195
|
+
```bash
|
196
|
+
sf org display --json
|
197
|
+
````
|
198
|
+
|
199
|
+
* For other apps, this value is returned during the **OAuth authorization flow**
|
200
|
+
* 📖 [Salesforce OAuth Flows Documentation](https://help.salesforce.com/s/articleView?id=xcloud.remoteaccess_oauth_flows.htm&type=5)
|
201
|
+
"""
|
202
|
+
return self._auth_manager.refresh_token
|
203
|
+
|
204
|
+
@property
|
205
|
+
def api_version(self) -> str:
|
206
|
+
"""
|
207
|
+
### `api_version`
|
208
|
+
|
209
|
+
**The Salesforce API version to use.**
|
210
|
+
|
211
|
+
* Must include the `"v"` prefix (e.g., `"v64.0"`)
|
212
|
+
* Periodically updated to align with new Salesforce releases
|
213
|
+
"""
|
214
|
+
return self._auth_manager.api_version
|
215
|
+
|
216
|
+
@property
|
217
|
+
def token_endpoint(self) -> str:
|
218
|
+
"""
|
219
|
+
### `token_endpoint`
|
220
|
+
|
221
|
+
**The token URL path for OAuth authentication.**
|
222
|
+
|
223
|
+
* Defaults to Salesforce's `.well-known/openid-configuration` for *token* endpoint
|
224
|
+
* Should start with a **leading slash**, e.g., `/services/oauth2/token`
|
225
|
+
* No customization is typical, but internal designs may use custom ApexRest endpoints
|
226
|
+
"""
|
227
|
+
return self._auth_manager.token_endpoint
|
228
|
+
|
229
|
+
@property
|
230
|
+
def access_token(self) -> Optional[str]:
|
231
|
+
"""
|
232
|
+
### `access_token`
|
233
|
+
|
234
|
+
**The current OAuth access token.**
|
235
|
+
|
236
|
+
* Used to authorize API requests
|
237
|
+
* Does not include Bearer prefix, strictly the token
|
238
|
+
"""
|
239
|
+
# refresh token if required
|
240
|
+
|
241
|
+
return self._auth_manager.access_token
|
242
|
+
|
243
|
+
@property
|
244
|
+
def token_expiration_time(self) -> Optional[float]:
|
245
|
+
"""
|
246
|
+
### `token_expiration_time`
|
247
|
+
|
248
|
+
**Unix timestamp (in seconds) for access token expiration.**
|
249
|
+
|
250
|
+
* Managed automatically by the library
|
251
|
+
* Useful for checking when to refresh the token
|
252
|
+
"""
|
253
|
+
return self._auth_manager.token_expiration_time
|
254
|
+
|
255
|
+
@property
|
256
|
+
def token_lifetime(self) -> int:
|
257
|
+
"""
|
258
|
+
### `token_lifetime`
|
259
|
+
|
260
|
+
**Access token lifespan in seconds.**
|
261
|
+
|
262
|
+
* Determined by your Connected App's session policies
|
263
|
+
* Used to calculate when to refresh the token
|
264
|
+
"""
|
265
|
+
return self._auth_manager.token_lifetime
|
266
|
+
|
267
|
+
@property
|
268
|
+
def user_agent(self) -> str:
|
269
|
+
"""
|
270
|
+
### `user_agent`
|
271
|
+
|
272
|
+
**Custom User-Agent string for API calls.**
|
273
|
+
|
274
|
+
* Included in HTTP request headers
|
275
|
+
* Useful for identifying traffic in Salesforce `ApiEvent` logs
|
276
|
+
"""
|
277
|
+
return self._http_client.user_agent
|
278
|
+
|
279
|
+
@property
|
280
|
+
def sforce_client(self) -> str:
|
281
|
+
"""
|
282
|
+
### `sforce_client`
|
283
|
+
|
284
|
+
**Custom application identifier.**
|
285
|
+
|
286
|
+
* Included in the `Sforce-Call-Options` header
|
287
|
+
* Useful for identifying traffic in Event Log Files
|
288
|
+
* Commas are not allowed; will be stripped
|
289
|
+
"""
|
290
|
+
return self._http_client.sforce_client
|
291
|
+
|
292
|
+
@property
|
293
|
+
def proxy(self) -> Optional[str]:
|
294
|
+
"""
|
295
|
+
### `proxy`
|
296
|
+
|
297
|
+
**The proxy configuration.**
|
298
|
+
|
299
|
+
* Proxy URL for HTTP requests
|
300
|
+
* None if no proxy is configured
|
301
|
+
"""
|
302
|
+
return self._auth_manager.get_proxy_config()
|
303
|
+
|
304
|
+
@property
|
305
|
+
def org_id(self) -> Optional[str]:
|
306
|
+
"""
|
307
|
+
### `org_id`
|
308
|
+
|
309
|
+
**The Salesforce organization ID.**
|
310
|
+
|
311
|
+
* Extracted from token response during authentication
|
312
|
+
* Available after successful token refresh
|
313
|
+
"""
|
314
|
+
return self._auth_manager.org_id
|
315
|
+
|
316
|
+
@property
|
317
|
+
def user_id(self) -> Optional[str]:
|
318
|
+
"""
|
319
|
+
### `user_id`
|
320
|
+
|
321
|
+
**The Salesforce user ID.**
|
322
|
+
|
323
|
+
* Extracted from token response during authentication
|
324
|
+
* Available after successful token refresh
|
325
|
+
"""
|
326
|
+
return self._auth_manager.user_id
|
327
|
+
|
328
|
+
# Token refresh method that delegates to HTTP client
|
329
|
+
def _refresh_token_if_needed(self) -> Optional[str]:
|
330
|
+
"""
|
331
|
+
Automatically refresh the access token if it has expired or is missing.
|
332
|
+
|
333
|
+
:return: A valid access token or None if refresh failed.
|
334
|
+
"""
|
335
|
+
return self._http_client.refresh_token_and_update_auth()
|
336
|
+
|
337
|
+
def read_static_resource_name(
|
338
|
+
self, resource_name: str, namespace: Optional[str] = None
|
339
|
+
) -> Optional[str]:
|
340
|
+
"""
|
341
|
+
Read a static resource for a given name from the Salesforce instance.
|
342
|
+
|
343
|
+
:param resource_name: Name of the static resource to read.
|
344
|
+
:param namespace: Namespace of the static resource to read (default is None).
|
345
|
+
:return: Static resource content or None on failure.
|
346
|
+
"""
|
347
|
+
return self._crud_client.read_static_resource_name(resource_name, namespace)
|
348
|
+
|
349
|
+
def read_static_resource_id(self, resource_id: str) -> Optional[str]:
|
350
|
+
"""
|
351
|
+
Read a static resource for a given ID from the Salesforce instance.
|
352
|
+
|
353
|
+
:param resource_id: ID of the static resource to read.
|
354
|
+
:return: Static resource content or None on failure.
|
355
|
+
"""
|
356
|
+
return self._crud_client.read_static_resource_id(resource_id)
|
357
|
+
|
358
|
+
def update_static_resource_name(
|
359
|
+
self, resource_name: str, data: str, namespace: Optional[str] = None
|
360
|
+
) -> Optional[Dict[str, Any]]:
|
361
|
+
"""
|
362
|
+
Update a static resource for a given name in the Salesforce instance.
|
363
|
+
|
364
|
+
:param resource_name: Name of the static resource to update.
|
365
|
+
:param data: Content to update the static resource with.
|
366
|
+
:param namespace: Optional namespace to search for the static resource.
|
367
|
+
:return: Static resource content or None on failure.
|
368
|
+
"""
|
369
|
+
return self._crud_client.update_static_resource_name(
|
370
|
+
resource_name, data, namespace
|
371
|
+
)
|
372
|
+
|
373
|
+
def update_static_resource_id(
|
374
|
+
self, resource_id: str, data: str
|
375
|
+
) -> Optional[Dict[str, Any]]:
|
376
|
+
"""
|
377
|
+
Replace the content of a static resource in the Salesforce instance by ID.
|
378
|
+
|
379
|
+
:param resource_id: ID of the static resource to update.
|
380
|
+
:param data: Content to update the static resource with.
|
381
|
+
:return: Parsed JSON response or None on failure.
|
382
|
+
"""
|
383
|
+
return self._crud_client.update_static_resource_id(resource_id, data)
|
384
|
+
|
385
|
+
def limits(self) -> Optional[Dict[str, Any]]:
|
386
|
+
"""
|
387
|
+
Fetch the current limits for the Salesforce instance.
|
388
|
+
|
389
|
+
:return: Parsed JSON response or None on failure.
|
390
|
+
"""
|
391
|
+
endpoint = f"/services/data/{self.api_version}/limits"
|
392
|
+
|
393
|
+
# Ensure we have a valid token
|
394
|
+
self._refresh_token_if_needed()
|
395
|
+
|
396
|
+
status, data = self._http_client.send_authenticated_request("GET", endpoint)
|
397
|
+
|
398
|
+
if status == 200:
|
399
|
+
import json
|
400
|
+
|
401
|
+
logger.debug("Limits fetched successfully.")
|
402
|
+
return json.loads(data)
|
403
|
+
|
404
|
+
logger.error("Failed to fetch limits: %s", status)
|
405
|
+
return None
|
406
|
+
|
407
|
+
def query(self, query: str, tooling: bool = False) -> Optional[Dict[str, Any]]:
|
408
|
+
"""
|
409
|
+
Execute a SOQL query using the REST or Tooling API.
|
410
|
+
|
411
|
+
:param query: The SOQL query string.
|
412
|
+
:param tooling: If True, use the Tooling API endpoint.
|
413
|
+
:return: Parsed JSON response or None on failure.
|
414
|
+
"""
|
415
|
+
return self._query_client.query(query, tooling)
|
416
|
+
|
417
|
+
def tooling_query(self, query: str) -> Optional[Dict[str, Any]]:
|
418
|
+
"""
|
419
|
+
Execute a SOQL query using the Tooling API.
|
420
|
+
|
421
|
+
:param query: The SOQL query string.
|
422
|
+
:return: Parsed JSON response or None on failure.
|
423
|
+
"""
|
424
|
+
return self._query_client.tooling_query(query)
|
425
|
+
|
426
|
+
def get_sobject_prefixes(
|
427
|
+
self, key_type: Literal["id", "name"] = "id"
|
428
|
+
) -> Optional[Dict[str, str]]:
|
429
|
+
"""
|
430
|
+
Fetch all key prefixes from the Salesforce instance and map them to sObject names or vice versa.
|
431
|
+
|
432
|
+
:param key_type: The type of key to return. Either 'id' (prefix) or 'name' (sObject).
|
433
|
+
:return: A dictionary mapping key prefixes to sObject names or None on failure.
|
434
|
+
"""
|
435
|
+
return self._query_client.get_sobject_prefixes(key_type)
|
436
|
+
|
437
|
+
def cquery(
|
438
|
+
self,
|
439
|
+
query_dict: Dict[str, str],
|
440
|
+
batch_size: int = 25,
|
441
|
+
max_workers: Optional[int] = None,
|
442
|
+
) -> Optional[Dict[str, Any]]:
|
443
|
+
"""
|
444
|
+
Execute multiple SOQL queries using the Composite Batch API with threading to reduce network overhead.
|
445
|
+
The function returns a dictionary mapping the original keys to their corresponding batch response.
|
446
|
+
The function requires a dictionary of SOQL queries with keys as logical names (referenceId) and values as SOQL queries.
|
447
|
+
Each query (subrequest) is counted as a unique API request against Salesforce governance limits.
|
448
|
+
|
449
|
+
:param query_dict: A dictionary of SOQL queries with keys as logical names and values as SOQL queries.
|
450
|
+
:param batch_size: The number of queries to include in each batch (default is 25).
|
451
|
+
:param max_workers: The maximum number of threads to spawn for concurrent execution (default is None).
|
452
|
+
:return: Dict mapping the original keys to their corresponding batch response or None on failure.
|
453
|
+
"""
|
454
|
+
return self._query_client.cquery(query_dict, batch_size, max_workers)
|
455
|
+
|
456
|
+
def cdelete(
|
457
|
+
self,
|
458
|
+
ids: Iterable[str],
|
459
|
+
batch_size: int = 200,
|
460
|
+
max_workers: Optional[int] = None,
|
461
|
+
) -> Optional[Dict[str, Any]]:
|
462
|
+
"""
|
463
|
+
Execute the Collections Delete API to delete multiple records using multithreading.
|
464
|
+
|
465
|
+
:param ids: A list of record IDs to delete.
|
466
|
+
:param batch_size: The number of records to delete in each batch (default is 200).
|
467
|
+
:param max_workers: The maximum number of threads to spawn for concurrent execution (default is None).
|
468
|
+
:return: Combined JSON response from all batches or None on complete failure.
|
469
|
+
"""
|
470
|
+
return self._crud_client.cdelete(ids, batch_size, max_workers)
|
471
|
+
|
472
|
+
def _cupdate(
|
473
|
+
self,
|
474
|
+
update_dict: Dict[str, Any],
|
475
|
+
batch_size: int = 25,
|
476
|
+
max_workers: Optional[int] = None,
|
477
|
+
) -> Optional[Dict[str, Any]]:
|
478
|
+
"""
|
479
|
+
Execute the Composite Update API to update multiple records.
|
480
|
+
|
481
|
+
:param update_dict: A dictionary of keys of records to be updated, and a dictionary of field-value pairs to be updated, with a special key '_' overriding the sObject type which is otherwise inferred from the key.
|
482
|
+
:param batch_size: The number of records to update in each batch (default is 25).
|
483
|
+
:param max_workers: The maximum number of threads to spawn for concurrent execution (default is None).
|
484
|
+
:return: JSON response from the update request or None on failure.
|
485
|
+
"""
|
486
|
+
return self._crud_client.cupdate(update_dict, batch_size, max_workers)
|
487
|
+
|
488
|
+
# SOAP methods delegated to SOAP client
|
489
|
+
def _gen_soap_envelope(self, header: str, body: str, api_type: str) -> str:
|
490
|
+
"""Generates a full SOAP envelope with all required namespaces for Salesforce API."""
|
491
|
+
return self._soap_client.generate_soap_envelope(header, body, api_type)
|
492
|
+
|
493
|
+
def _gen_soap_header(self) -> str:
|
494
|
+
"""This function generates the header for the SOAP request."""
|
495
|
+
# Ensure we have a valid token
|
496
|
+
self._refresh_token_if_needed()
|
497
|
+
return self._soap_client.generate_soap_header(self.access_token)
|
498
|
+
|
499
|
+
def _extract_soap_result_fields(self, xml_string: str) -> Optional[Dict[str, Any]]:
|
500
|
+
"""Parse SOAP XML and extract all child fields from <result> as a dict."""
|
501
|
+
return self._soap_client.extract_soap_result_fields(xml_string)
|
502
|
+
|
503
|
+
def _gen_soap_body(self, sobject: str, method: str, data: Dict[str, Any]) -> str:
|
504
|
+
"""Generates a compact SOAP request body for one or more records."""
|
505
|
+
return self._soap_client.generate_soap_body(sobject, method, data)
|
506
|
+
|
507
|
+
def _xml_to_json(self, xml_string: str) -> Optional[Dict[str, Any]]:
|
508
|
+
"""Convert an XML string to a JSON-like dictionary."""
|
509
|
+
return self._soap_client.xml_to_dict(xml_string)
|
510
|
+
|
511
|
+
def _xml_to_dict(self, element) -> Dict[str, Any]:
|
512
|
+
"""Recursively convert an XML Element to a dictionary."""
|
513
|
+
return self._soap_client._xml_element_to_dict(element)
|
514
|
+
|
515
|
+
def _create( # I don't like this name, will think of a better one later...as such, not public.
|
516
|
+
self,
|
517
|
+
sobject: str,
|
518
|
+
insert_list: List[Dict[str, Any]],
|
519
|
+
batch_size: int = 200,
|
520
|
+
max_workers: Optional[int] = None,
|
521
|
+
api_type: Literal["enterprise", "tooling"] = "enterprise",
|
522
|
+
) -> Optional[Dict[str, Any]]:
|
523
|
+
"""
|
524
|
+
Execute the Insert API to insert multiple records via SOAP calls.
|
525
|
+
|
526
|
+
:param sobject: The name of the sObject to insert into.
|
527
|
+
:param insert_list: A list of dictionaries, each representing a record to insert.
|
528
|
+
:param batch_size: The number of records to insert in each batch (default is 200).
|
529
|
+
:param max_workers: The maximum number of threads to spawn for concurrent execution (default is None).
|
530
|
+
:param api_type: API type to use ('enterprise' or 'tooling').
|
531
|
+
:return: JSON response from the insert request or None on failure.
|
532
|
+
"""
|
533
|
+
return self._crud_client.create(
|
534
|
+
sobject, insert_list, batch_size, max_workers, api_type
|
535
|
+
)
|
536
|
+
|
537
|
+
def debug_cleanup(
|
538
|
+
self,
|
539
|
+
apex_logs: bool = True,
|
540
|
+
expired_apex_flags: bool = True,
|
541
|
+
all_apex_flags: bool = False,
|
542
|
+
) -> None:
|
543
|
+
"""
|
544
|
+
Perform cleanup operations for Apex debug logs.
|
545
|
+
"""
|
546
|
+
self._debug_cleanup.debug_cleanup(
|
547
|
+
apex_logs=apex_logs,
|
548
|
+
expired_apex_flags=expired_apex_flags,
|
549
|
+
all_apex_flags=all_apex_flags,
|
550
|
+
)
|
551
|
+
|
552
|
+
def open_frontdoor(self) -> None:
|
553
|
+
"""
|
554
|
+
This function opens the Salesforce Frontdoor URL in the default web browser.
|
555
|
+
"""
|
556
|
+
self._refresh_token_if_needed()
|
557
|
+
if not self.access_token:
|
558
|
+
logger.error("No access token available for frontdoor URL")
|
559
|
+
return
|
560
|
+
|
561
|
+
sid = quote(self.access_token, safe="")
|
562
|
+
frontdoor_url = f"{self.instance_url}/secur/frontdoor.jsp?sid={sid}"
|
563
|
+
webbrowser.open(frontdoor_url)
|
@@ -2,13 +2,14 @@ import http.client
|
|
2
2
|
import json
|
3
3
|
import logging
|
4
4
|
import time
|
5
|
-
from typing import Any, Optional
|
6
5
|
import warnings
|
7
6
|
from queue import Empty, Queue
|
7
|
+
from typing import Any, Optional
|
8
8
|
|
9
9
|
TRACE = 5
|
10
10
|
logging.addLevelName(TRACE, "TRACE")
|
11
11
|
|
12
|
+
|
12
13
|
class ExperimentalWarning(Warning):
|
13
14
|
pass
|
14
15
|
|
@@ -69,6 +70,7 @@ def trace(self: logging.Logger, message: str, *args: Any, **kwargs: Any) -> None
|
|
69
70
|
logging.Logger.trace = trace
|
70
71
|
logger = logging.getLogger("sfq")
|
71
72
|
|
73
|
+
|
72
74
|
def _reconnect_with_backoff(self, attempt: int) -> None:
|
73
75
|
wait_time = min(2**attempt, 60)
|
74
76
|
logger.warning(
|
@@ -76,6 +78,7 @@ def _reconnect_with_backoff(self, attempt: int) -> None:
|
|
76
78
|
)
|
77
79
|
time.sleep(wait_time)
|
78
80
|
|
81
|
+
|
79
82
|
def _subscribe_topic(
|
80
83
|
self,
|
81
84
|
topic: str,
|
@@ -141,9 +144,7 @@ def _subscribe_topic(
|
|
141
144
|
logger.trace("Received handshake response.")
|
142
145
|
for name, value in response.getheaders():
|
143
146
|
if name.lower() == "set-cookie" and "BAYEUX_BROWSER=" in value:
|
144
|
-
_bayeux_browser_cookie = value.split("BAYEUX_BROWSER=")[1].split(
|
145
|
-
";"
|
146
|
-
)[0]
|
147
|
+
_bayeux_browser_cookie = value.split("BAYEUX_BROWSER=")[1].split(";")[0]
|
147
148
|
headers["Cookie"] = f"BAYEUX_BROWSER={_bayeux_browser_cookie}"
|
148
149
|
break
|
149
150
|
|
@@ -232,9 +233,7 @@ def _subscribe_topic(
|
|
232
233
|
ConnectionRefusedError,
|
233
234
|
ConnectionError,
|
234
235
|
) as e:
|
235
|
-
logger.warning(
|
236
|
-
f"Connection error (attempt {attempt + 1}): {e}"
|
237
|
-
)
|
236
|
+
logger.warning(f"Connection error (attempt {attempt + 1}): {e}")
|
238
237
|
conn.close()
|
239
238
|
conn = self._create_connection(parsed_url.netloc)
|
240
239
|
self._reconnect_with_backoff(attempt)
|
@@ -266,9 +265,7 @@ def _subscribe_topic(
|
|
266
265
|
finally:
|
267
266
|
if client_id:
|
268
267
|
try:
|
269
|
-
logger.trace(
|
270
|
-
f"Disconnecting from server with client ID: {client_id}"
|
271
|
-
)
|
268
|
+
logger.trace(f"Disconnecting from server with client ID: {client_id}")
|
272
269
|
disconnect_payload = json.dumps(
|
273
270
|
[
|
274
271
|
{
|