sfq 0.0.15__py3-none-any.whl → 0.0.16__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
sfq/__init__.py CHANGED
@@ -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.15",
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.15").
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 self.access_token and self.token_expiration_time is None:
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 = 10
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 max_workers: The maximum number of threads to spawn for concurrent execution (default is 10).
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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sfq
3
- Version: 0.0.15
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
@@ -0,0 +1,6 @@
1
+ sfq/__init__.py,sha256=8OErEOUxTDKOoj4sFyovhBXi0m918s_G1gaHerqTwzE,31967
2
+ sfq/_cometd.py,sha256=XimQEubmJwUmbWe85TxH_cuhGvWVuiHHrVr41tguuiI,10508
3
+ sfq/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
+ sfq-0.0.16.dist-info/METADATA,sha256=_rW37RIboasxVhI0wJ0GkSK_IYtd7ld5MJtZLqc_Vpg,6908
5
+ sfq-0.0.16.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
6
+ sfq-0.0.16.dist-info/RECORD,,
@@ -1,6 +0,0 @@
1
- sfq/__init__.py,sha256=b70qbaov94JC7qWHuJA6X0i6O-H145YS-_vlyPzWig4,29895
2
- sfq/_cometd.py,sha256=XimQEubmJwUmbWe85TxH_cuhGvWVuiHHrVr41tguuiI,10508
3
- sfq/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
- sfq-0.0.15.dist-info/METADATA,sha256=ipG9mLqnwZwGp6gUGSbggP_LNl80YcGPpM1_fYlS7Vo,6598
5
- sfq-0.0.15.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
6
- sfq-0.0.15.dist-info/RECORD,,
File without changes