sfq 0.0.15__tar.gz → 0.0.16__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.15 → sfq-0.0.16}/PKG-INFO +8 -1
- {sfq-0.0.15 → sfq-0.0.16}/README.md +7 -0
- {sfq-0.0.15 → sfq-0.0.16}/pyproject.toml +1 -1
- {sfq-0.0.15 → sfq-0.0.16}/src/sfq/__init__.py +60 -19
- {sfq-0.0.15 → sfq-0.0.16}/uv.lock +1 -1
- {sfq-0.0.15 → sfq-0.0.16}/.github/workflows/publish.yml +0 -0
- {sfq-0.0.15 → sfq-0.0.16}/.gitignore +0 -0
- {sfq-0.0.15 → sfq-0.0.16}/.python-version +0 -0
- {sfq-0.0.15 → sfq-0.0.16}/src/sfq/_cometd.py +0 -0
- {sfq-0.0.15 → sfq-0.0.16}/src/sfq/py.typed +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: sfq
|
3
|
-
Version: 0.0.
|
3
|
+
Version: 0.0.16
|
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
|
@@ -90,6 +90,13 @@ for subrequest_identifer, subrequest_response in batched_response.items():
|
|
90
90
|
>>> "Frozen Users" returned 4082 records
|
91
91
|
```
|
92
92
|
|
93
|
+
### Collection Deletions
|
94
|
+
|
95
|
+
```python
|
96
|
+
response = sf.cdelete(['07La0000000bYgj', '07La0000000bYgk', '07La0000000bYgl'])
|
97
|
+
>>> [{'id': '500aj000006wtdZAAQ', 'success': True, 'errors': []}, {'id': '500aj000006wtdaAAA', 'success': True, 'errors': []}, {'id': '500aj000006wtdbAAA', 'success': True, 'errors': []}]
|
98
|
+
```
|
99
|
+
|
93
100
|
### Static Resources
|
94
101
|
|
95
102
|
```python
|
@@ -74,6 +74,13 @@ for subrequest_identifer, subrequest_response in batched_response.items():
|
|
74
74
|
>>> "Frozen Users" returned 4082 records
|
75
75
|
```
|
76
76
|
|
77
|
+
### Collection Deletions
|
78
|
+
|
79
|
+
```python
|
80
|
+
response = sf.cdelete(['07La0000000bYgj', '07La0000000bYgk', '07La0000000bYgl'])
|
81
|
+
>>> [{'id': '500aj000006wtdZAAQ', 'success': True, 'errors': []}, {'id': '500aj000006wtdaAAA', 'success': True, 'errors': []}, {'id': '500aj000006wtdbAAA', 'success': True, 'errors': []}]
|
82
|
+
```
|
83
|
+
|
77
84
|
### Static Resources
|
78
85
|
|
79
86
|
```python
|
@@ -7,17 +7,13 @@ import time
|
|
7
7
|
import warnings
|
8
8
|
from collections import OrderedDict
|
9
9
|
from concurrent.futures import ThreadPoolExecutor, as_completed
|
10
|
-
from typing import Any, Dict, Literal, Optional, List, Tuple
|
10
|
+
from typing import Any, Dict, Iterable, Literal, Optional, List, Tuple
|
11
11
|
from urllib.parse import quote, urlparse
|
12
12
|
|
13
13
|
TRACE = 5
|
14
14
|
logging.addLevelName(TRACE, "TRACE")
|
15
15
|
|
16
16
|
|
17
|
-
class ExperimentalWarning(Warning):
|
18
|
-
pass
|
19
|
-
|
20
|
-
|
21
17
|
def trace(self: logging.Logger, message: str, *args: Any, **kwargs: Any) -> None:
|
22
18
|
"""Custom TRACE level logging function with redaction."""
|
23
19
|
|
@@ -88,7 +84,7 @@ class SFAuth:
|
|
88
84
|
access_token: Optional[str] = None,
|
89
85
|
token_expiration_time: Optional[float] = None,
|
90
86
|
token_lifetime: int = 15 * 60,
|
91
|
-
user_agent: str = "sfq/0.0.
|
87
|
+
user_agent: str = "sfq/0.0.16",
|
92
88
|
sforce_client: str = "_auto",
|
93
89
|
proxy: str = "auto",
|
94
90
|
) -> None:
|
@@ -104,7 +100,7 @@ class SFAuth:
|
|
104
100
|
:param access_token: The access token for the current session (default is None).
|
105
101
|
:param token_expiration_time: The expiration time of the access token (default is None).
|
106
102
|
:param token_lifetime: The lifetime of the access token in seconds (default is 15 minutes).
|
107
|
-
:param user_agent: Custom User-Agent string (default is "sfq/0.0.
|
103
|
+
:param user_agent: Custom User-Agent string (default is "sfq/0.0.16").
|
108
104
|
:param sforce_client: Custom Application Identifier (default is user_agent).
|
109
105
|
:param proxy: The proxy configuration, "auto" to use environment (default is "auto").
|
110
106
|
"""
|
@@ -118,12 +114,12 @@ class SFAuth:
|
|
118
114
|
self.token_expiration_time = token_expiration_time
|
119
115
|
self.token_lifetime = token_lifetime
|
120
116
|
self.user_agent = user_agent
|
121
|
-
self.sforce_client = sforce_client
|
117
|
+
self.sforce_client = quote(str(sforce_client), safe="")
|
122
118
|
self._auto_configure_proxy(proxy)
|
123
119
|
self._high_api_usage_threshold = 80
|
124
120
|
|
125
121
|
if sforce_client == "_auto":
|
126
|
-
self.sforce_client = user_agent
|
122
|
+
self.sforce_client = quote(str(user_agent), safe="")
|
127
123
|
|
128
124
|
if self.client_secret == "_deprecation_warning":
|
129
125
|
warnings.warn(
|
@@ -210,7 +206,6 @@ class SFAuth:
|
|
210
206
|
endpoint: str,
|
211
207
|
headers: Dict[str, str],
|
212
208
|
body: Optional[str] = None,
|
213
|
-
timeout: Optional[int] = None,
|
214
209
|
) -> Tuple[Optional[int], Optional[str]]:
|
215
210
|
"""
|
216
211
|
Unified request method with built-in logging and error handling.
|
@@ -256,7 +251,7 @@ class SFAuth:
|
|
256
251
|
:param payload: Payload for the token request.
|
257
252
|
:return: Parsed JSON response or None on failure.
|
258
253
|
"""
|
259
|
-
headers = self._get_common_headers()
|
254
|
+
headers = self._get_common_headers(recursive_call=True)
|
260
255
|
headers["Content-Type"] = "application/x-www-form-urlencoded"
|
261
256
|
del headers["Authorization"]
|
262
257
|
|
@@ -343,16 +338,15 @@ class SFAuth:
|
|
343
338
|
logger.error("Failed to obtain access token.")
|
344
339
|
return None
|
345
340
|
|
346
|
-
def _get_common_headers(self) -> Dict[str, str]:
|
341
|
+
def _get_common_headers(self, recursive_call: bool = False) -> Dict[str, str]:
|
347
342
|
"""
|
348
343
|
Generate common headers for API requests.
|
349
344
|
|
350
345
|
:return: A dictionary of common headers.
|
351
346
|
"""
|
352
|
-
if not
|
353
|
-
self.token_expiration_time = int(time.time())
|
347
|
+
if not recursive_call:
|
354
348
|
self._refresh_token_if_needed()
|
355
|
-
|
349
|
+
|
356
350
|
return {
|
357
351
|
"Authorization": f"Bearer {self.access_token}",
|
358
352
|
"User-Agent": self.user_agent,
|
@@ -361,8 +355,6 @@ class SFAuth:
|
|
361
355
|
"Content-Type": "application/json",
|
362
356
|
}
|
363
357
|
|
364
|
-
|
365
|
-
|
366
358
|
def _is_token_expired(self) -> bool:
|
367
359
|
"""
|
368
360
|
Check if the access token has expired.
|
@@ -648,7 +640,7 @@ class SFAuth:
|
|
648
640
|
return None
|
649
641
|
|
650
642
|
def cquery(
|
651
|
-
self, query_dict: dict[str, str], max_workers: int =
|
643
|
+
self, query_dict: dict[str, str], batch_size: int = 25, max_workers: int = None
|
652
644
|
) -> Optional[Dict[str, Any]]:
|
653
645
|
"""
|
654
646
|
Execute multiple SOQL queries using the Composite Batch API with threading to reduce network overhead.
|
@@ -657,7 +649,8 @@ class SFAuth:
|
|
657
649
|
Each query (subrequest) is counted as a unique API request against Salesforce governance limits.
|
658
650
|
|
659
651
|
:param query_dict: A dictionary of SOQL queries with keys as logical names and values as SOQL queries.
|
660
|
-
:param
|
652
|
+
:param batch_size: The number of queries to include in each batch (default is 25).
|
653
|
+
:param max_workers: The maximum number of threads to spawn for concurrent execution (default is None).
|
661
654
|
:return: Dict mapping the original keys to their corresponding batch response or None on failure.
|
662
655
|
"""
|
663
656
|
if not query_dict:
|
@@ -757,3 +750,51 @@ class SFAuth:
|
|
757
750
|
|
758
751
|
logger.trace("Composite query results: %s", results_dict)
|
759
752
|
return results_dict
|
753
|
+
|
754
|
+
def cdelete(
|
755
|
+
self, ids: Iterable[str], batch_size: int = 200, max_workers: int = None
|
756
|
+
) -> Optional[Dict[str, Any]]:
|
757
|
+
"""
|
758
|
+
Execute the Collections Delete API to delete multiple records using multithreading.
|
759
|
+
|
760
|
+
:param ids: A list of record IDs to delete.
|
761
|
+
:param batch_size: The number of records to delete in each batch (default is 200).
|
762
|
+
:param max_workers: The maximum number of threads to spawn for concurrent execution (default is None).
|
763
|
+
:return: Combined JSON response from all batches or None on complete failure.
|
764
|
+
"""
|
765
|
+
ids = list(ids)
|
766
|
+
chunks = [ids[i : i + batch_size] for i in range(0, len(ids), batch_size)]
|
767
|
+
|
768
|
+
def delete_chunk(chunk: List[str]) -> Optional[Dict[str, Any]]:
|
769
|
+
endpoint = f"/services/data/{self.api_version}/composite/sobjects?ids={','.join(chunk)}&allOrNone=false"
|
770
|
+
headers = self._get_common_headers()
|
771
|
+
|
772
|
+
status_code, resp_data = self._send_request(
|
773
|
+
method="DELETE",
|
774
|
+
endpoint=endpoint,
|
775
|
+
headers=headers,
|
776
|
+
)
|
777
|
+
|
778
|
+
if status_code == 200:
|
779
|
+
logger.debug("Collections delete API response without errors.")
|
780
|
+
return json.loads(resp_data)
|
781
|
+
else:
|
782
|
+
logger.error("Collections delete API request failed: %s", status_code)
|
783
|
+
logger.debug("Response body: %s", resp_data)
|
784
|
+
return None
|
785
|
+
|
786
|
+
results = []
|
787
|
+
with ThreadPoolExecutor(max_workers=max_workers) as executor:
|
788
|
+
futures = [executor.submit(delete_chunk, chunk) for chunk in chunks]
|
789
|
+
for future in as_completed(futures):
|
790
|
+
result = future.result()
|
791
|
+
if result:
|
792
|
+
results.append(result)
|
793
|
+
|
794
|
+
combined_response = [
|
795
|
+
item
|
796
|
+
for result in results
|
797
|
+
for item in (result if isinstance(result, list) else [result])
|
798
|
+
if isinstance(result, (dict, list))
|
799
|
+
]
|
800
|
+
return combined_response or None
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|