sfq 0.0.11__py3-none-any.whl → 0.0.12__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
@@ -5,6 +5,8 @@ import logging
5
5
  import os
6
6
  import time
7
7
  import warnings
8
+ from collections import OrderedDict
9
+ from concurrent.futures import ThreadPoolExecutor, as_completed
8
10
  from queue import Empty, Queue
9
11
  from typing import Any, Dict, Optional
10
12
  from urllib.parse import quote, urlparse
@@ -12,9 +14,11 @@ from urllib.parse import quote, urlparse
12
14
  TRACE = 5
13
15
  logging.addLevelName(TRACE, "TRACE")
14
16
 
17
+
15
18
  class ExperimentalWarning(Warning):
16
19
  pass
17
20
 
21
+
18
22
  def trace(self: logging.Logger, message: str, *args: Any, **kwargs: Any) -> None:
19
23
  """Custom TRACE level logging function with redaction."""
20
24
 
@@ -83,7 +87,7 @@ class SFAuth:
83
87
  access_token: Optional[str] = None,
84
88
  token_expiration_time: Optional[float] = None,
85
89
  token_lifetime: int = 15 * 60,
86
- user_agent: str = "sfq/0.0.11",
90
+ user_agent: str = "sfq/0.0.12",
87
91
  proxy: str = "auto",
88
92
  ) -> None:
89
93
  """
@@ -97,7 +101,7 @@ class SFAuth:
97
101
  :param access_token: The access token for the current session (default is None).
98
102
  :param token_expiration_time: The expiration time of the access token (default is None).
99
103
  :param token_lifetime: The lifetime of the access token in seconds (default is 15 minutes).
100
- :param user_agent: Custom User-Agent string (default is "sfq/0.0.11").
104
+ :param user_agent: Custom User-Agent string (default is "sfq/0.0.12").
101
105
  :param proxy: The proxy configuration, "auto" to use environment (default is "auto").
102
106
  """
103
107
  self.instance_url = instance_url
@@ -603,14 +607,108 @@ class SFAuth:
603
607
  """
604
608
  return self.query(query, tooling=True)
605
609
 
606
- def _reconnect_with_backoff(self, attempt: int) -> None:
607
- wait_time = min(2**attempt, 60)
608
- logger.warning(
609
- f"Reconnecting after failure, backoff {wait_time}s (attempt {attempt})"
610
- )
611
- time.sleep(wait_time)
610
+ def cquery(self, query_dict: dict[str, str], max_workers: int = 10) -> Optional[Dict[str, Any]]:
611
+ """
612
+ Execute multiple SOQL queries using the Composite Batch API with threading to reduce network overhead.
613
+ The function returns a dictionary mapping the original keys to their corresponding batch response.
614
+ The function requires a dictionary of SOQL queries with keys as logical names (referenceId) and values as SOQL queries.
615
+ Each query (subrequest) is counted as a unique API request against Salesforce governance limits.
616
+
617
+ :param query_dict: A dictionary of SOQL queries with keys as logical names and values as SOQL queries.
618
+ :param max_workers: The maximum number of threads to spawn for concurrent execution (default is 10).
619
+ :return: Dict mapping the original keys to their corresponding batch response or None on failure.
620
+ """
621
+ if not query_dict:
622
+ logger.warning("No queries to execute.")
623
+ return None
624
+
612
625
  self._refresh_token_if_needed()
613
626
 
627
+ if not self.access_token:
628
+ logger.error("No access token available for query.")
629
+ return None
630
+
631
+ def _execute_batch(queries_batch):
632
+ endpoint = f"/services/data/{self.api_version}/composite/batch"
633
+ headers = {
634
+ "Authorization": f"Bearer {self.access_token}",
635
+ "User-Agent": self.user_agent,
636
+ "Accept": "application/json",
637
+ "Content-Type": "application/json",
638
+ }
639
+
640
+ payload = {
641
+ "haltOnError": False,
642
+ "batchRequests": [
643
+ {
644
+ "method": "GET",
645
+ "url": f"/services/data/{self.api_version}/query?q={quote(query)}",
646
+ }
647
+ for query in queries_batch
648
+ ],
649
+ }
650
+
651
+ parsed_url = urlparse(self.instance_url)
652
+ conn = self._create_connection(parsed_url.netloc)
653
+ batch_results = {}
654
+
655
+ try:
656
+ logger.trace("Request endpoint: %s", endpoint)
657
+ logger.trace("Request headers: %s", headers)
658
+ logger.trace("Request payload: %s", json.dumps(payload, indent=2))
659
+
660
+ conn.request("POST", endpoint, json.dumps(payload), headers=headers)
661
+ conn.sock.settimeout(60 * 10)
662
+ response = conn.getresponse()
663
+ data = response.read().decode("utf-8")
664
+ self._http_resp_header_logic(response)
665
+
666
+ if response.status == 200:
667
+ logger.debug("Composite query successful.")
668
+ logger.trace("Composite query full response: %s", data)
669
+ results = json.loads(data).get("results", [])
670
+ for i, result in enumerate(results):
671
+ batch_results[keys[i]] = result
672
+ if result.get("statusCode") != 200:
673
+ logger.error("Query failed for key %s: %s", keys[i], result)
674
+ logger.error(
675
+ "Query failed with HTTP status %s (%s)",
676
+ result.get("statusCode"),
677
+ result.get("statusMessage"),
678
+ )
679
+ logger.trace("Query response: %s", result)
680
+ else:
681
+ logger.error(
682
+ "Composite query failed with HTTP status %s (%s)",
683
+ response.status,
684
+ response.reason,
685
+ )
686
+ batch_results[keys[i]] = data
687
+ logger.trace("Composite query response: %s", data)
688
+ except Exception as err:
689
+ logger.exception("Exception during composite query: %s", err)
690
+ finally:
691
+ logger.trace("Closing connection...")
692
+ conn.close()
693
+
694
+ return batch_results
695
+
696
+ keys = list(query_dict.keys())
697
+ results_dict = OrderedDict()
698
+
699
+ with ThreadPoolExecutor(max_workers=max_workers) as executor:
700
+ futures = []
701
+ for i in range(0, len(keys), 25):
702
+ batch_keys = keys[i : i + 25]
703
+ batch_queries = [query_dict[key] for key in batch_keys]
704
+ futures.append(executor.submit(_execute_batch, batch_queries))
705
+
706
+ for future in as_completed(futures):
707
+ results_dict.update(future.result())
708
+
709
+ logger.trace("Composite query results: %s", results_dict)
710
+ return results_dict
711
+
614
712
  def _subscribe_topic(
615
713
  self,
616
714
  topic: str,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sfq
3
- Version: 0.0.11
3
+ Version: 0.0.12
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
@@ -0,0 +1,5 @@
1
+ sfq/__init__.py,sha256=XEKDLgvHBwGfIU0TDQjyxgLnfiFyCtd0a_MW3DnqFxQ,36032
2
+ sfq/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
+ sfq-0.0.12.dist-info/METADATA,sha256=glq9BPwFsvXAk1yaK30GrbdT3oDdl2Urd3lpdc3JW0s,5067
4
+ sfq-0.0.12.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
5
+ sfq-0.0.12.dist-info/RECORD,,
@@ -1,5 +0,0 @@
1
- sfq/__init__.py,sha256=QkMVcIOaQO7XP29zK9l7uQCTx0NcqIDBNj8V08Llu24,31595
2
- sfq/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
- sfq-0.0.11.dist-info/METADATA,sha256=F1sqI833stEXIk9Z7jtG6q0F8dPuEGMDW8xgFgGxa4M,5067
4
- sfq-0.0.11.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
5
- sfq-0.0.11.dist-info/RECORD,,
File without changes