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.
Files changed (42) hide show
  1. {sfq-0.0.32 → sfq-0.0.34}/.github/workflows/publish.yml +7 -1
  2. {sfq-0.0.32 → sfq-0.0.34}/PKG-INFO +1 -1
  3. {sfq-0.0.32 → sfq-0.0.34}/pyproject.toml +1 -1
  4. sfq-0.0.34/src/sfq/__init__.py +563 -0
  5. {sfq-0.0.32 → sfq-0.0.34}/src/sfq/_cometd.py +7 -10
  6. sfq-0.0.34/src/sfq/auth.py +401 -0
  7. sfq-0.0.34/src/sfq/crud.py +514 -0
  8. sfq-0.0.34/src/sfq/debug_cleanup.py +71 -0
  9. sfq-0.0.34/src/sfq/exceptions.py +54 -0
  10. sfq-0.0.34/src/sfq/http_client.py +319 -0
  11. sfq-0.0.34/src/sfq/query.py +398 -0
  12. sfq-0.0.34/src/sfq/soap.py +186 -0
  13. sfq-0.0.34/src/sfq/utils.py +196 -0
  14. sfq-0.0.34/tests/test_auth.py +894 -0
  15. {sfq-0.0.32 → sfq-0.0.34}/tests/test_cdelete.py +7 -10
  16. sfq-0.0.34/tests/test_compatibility.py +668 -0
  17. {sfq-0.0.32 → sfq-0.0.34}/tests/test_cquery.py +16 -12
  18. {sfq-0.0.32 → sfq-0.0.34}/tests/test_create.py +4 -11
  19. sfq-0.0.34/tests/test_crud.py +640 -0
  20. sfq-0.0.34/tests/test_crud_e2e.py +606 -0
  21. {sfq-0.0.32 → sfq-0.0.34}/tests/test_cupdate.py +60 -28
  22. sfq-0.0.32/tests/test_debug_cleanup.py → sfq-0.0.34/tests/test_debug_cleanup_e2e.py +47 -14
  23. sfq-0.0.34/tests/test_debug_cleanup_unit.py +57 -0
  24. sfq-0.0.34/tests/test_http_client.py +513 -0
  25. {sfq-0.0.32 → sfq-0.0.34}/tests/test_limits_api.py +2 -8
  26. {sfq-0.0.32 → sfq-0.0.34}/tests/test_log_trace_redact.py +17 -11
  27. {sfq-0.0.32 → sfq-0.0.34}/tests/test_open_frontdoor.py +2 -8
  28. sfq-0.0.34/tests/test_query.py +67 -0
  29. sfq-0.0.34/tests/test_query_client.py +887 -0
  30. sfq-0.0.34/tests/test_query_e2e.py +363 -0
  31. sfq-0.0.34/tests/test_query_integration.py +277 -0
  32. sfq-0.0.34/tests/test_soap.py +390 -0
  33. sfq-0.0.34/tests/test_soap_batch_operation.py +42 -0
  34. sfq-0.0.34/tests/test_static_resources.py +100 -0
  35. sfq-0.0.34/tests/test_utils.py +229 -0
  36. {sfq-0.0.32 → sfq-0.0.34}/uv.lock +1 -1
  37. sfq-0.0.32/src/sfq/__init__.py +0 -1237
  38. sfq-0.0.32/tests/test_query.py +0 -126
  39. {sfq-0.0.32 → sfq-0.0.34}/.gitignore +0 -0
  40. {sfq-0.0.32 → sfq-0.0.34}/.python-version +0 -0
  41. {sfq-0.0.32 → sfq-0.0.34}/README.md +0 -0
  42. {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 src/sfq/__init__.py -o docs --no-search
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: |
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sfq
3
- Version: 0.0.32
3
+ Version: 0.0.34
4
4
  Summary: Python wrapper for the Salesforce's Query API.
5
5
  Author-email: David Moruzzi <sfq.pypi@dmoruzi.com>
6
6
  Keywords: salesforce,salesforce query
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "sfq"
3
- version = "0.0.32"
3
+ version = "0.0.34"
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" }]
@@ -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
  {