sfq 0.0.19__tar.gz → 0.0.21__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.19 → sfq-0.0.21}/PKG-INFO +1 -1
- {sfq-0.0.19 → sfq-0.0.21}/pyproject.toml +1 -1
- {sfq-0.0.19 → sfq-0.0.21}/src/sfq/__init__.py +72 -92
- {sfq-0.0.19 → sfq-0.0.21}/uv.lock +1 -1
- {sfq-0.0.19 → sfq-0.0.21}/.github/workflows/publish.yml +0 -0
- {sfq-0.0.19 → sfq-0.0.21}/.gitignore +0 -0
- {sfq-0.0.19 → sfq-0.0.21}/.python-version +0 -0
- {sfq-0.0.19 → sfq-0.0.21}/README.md +0 -0
- {sfq-0.0.19 → sfq-0.0.21}/src/sfq/_cometd.py +0 -0
- {sfq-0.0.19 → sfq-0.0.21}/src/sfq/py.typed +0 -0
@@ -79,12 +79,12 @@ class SFAuth:
|
|
79
79
|
client_id: str,
|
80
80
|
refresh_token: str, # client_secret & refresh_token will swap positions 2025-AUG-1
|
81
81
|
client_secret: str = "_deprecation_warning", # mandatory after 2025-AUG-1
|
82
|
-
api_version: str = "
|
82
|
+
api_version: str = "v64.0",
|
83
83
|
token_endpoint: str = "/services/oauth2/token",
|
84
84
|
access_token: Optional[str] = None,
|
85
85
|
token_expiration_time: Optional[float] = None,
|
86
86
|
token_lifetime: int = 15 * 60,
|
87
|
-
user_agent: str = "sfq/0.0.
|
87
|
+
user_agent: str = "sfq/0.0.21",
|
88
88
|
sforce_client: str = "_auto",
|
89
89
|
proxy: str = "_auto",
|
90
90
|
) -> None:
|
@@ -95,12 +95,12 @@ class SFAuth:
|
|
95
95
|
:param client_id: The client ID for OAuth.
|
96
96
|
:param refresh_token: The refresh token for OAuth.
|
97
97
|
:param client_secret: The client secret for OAuth (default is "_deprecation_warning").
|
98
|
-
:param api_version: The Salesforce API version (default is "
|
98
|
+
:param api_version: The Salesforce API version (default is "v64.0").
|
99
99
|
:param token_endpoint: The token endpoint (default is "/services/oauth2/token").
|
100
100
|
:param access_token: The access token for the current session (default is None).
|
101
101
|
:param token_expiration_time: The expiration time of the access token (default is None).
|
102
102
|
:param token_lifetime: The lifetime of the access token in seconds (default is 15 minutes).
|
103
|
-
: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.21").
|
104
104
|
:param sforce_client: Custom Application Identifier (default is user_agent).
|
105
105
|
:param proxy: The proxy configuration, "_auto" to use environment (default is "_auto").
|
106
106
|
"""
|
@@ -510,6 +510,40 @@ class SFAuth:
|
|
510
510
|
logger.error("Failed to fetch limits: %s", status)
|
511
511
|
return None
|
512
512
|
|
513
|
+
def _paginate_query_result(self, initial_result: dict, headers: dict) -> dict:
|
514
|
+
"""
|
515
|
+
Helper to paginate Salesforce query results (for both query and cquery).
|
516
|
+
Returns a dict with all records combined.
|
517
|
+
"""
|
518
|
+
records = list(initial_result.get("records", []))
|
519
|
+
done = initial_result.get("done", True)
|
520
|
+
next_url = initial_result.get("nextRecordsUrl")
|
521
|
+
total_size = initial_result.get("totalSize", len(records))
|
522
|
+
|
523
|
+
while not done and next_url:
|
524
|
+
status_code, data = self._send_request(
|
525
|
+
method="GET",
|
526
|
+
endpoint=next_url,
|
527
|
+
headers=headers,
|
528
|
+
)
|
529
|
+
if status_code == 200:
|
530
|
+
next_result = json.loads(data)
|
531
|
+
records.extend(next_result.get("records", []))
|
532
|
+
done = next_result.get("done", True)
|
533
|
+
next_url = next_result.get("nextRecordsUrl")
|
534
|
+
total_size = next_result.get("totalSize", total_size)
|
535
|
+
else:
|
536
|
+
logger.error("Failed to fetch next records: %s", data)
|
537
|
+
break
|
538
|
+
|
539
|
+
paginated = dict(initial_result)
|
540
|
+
paginated["records"] = records
|
541
|
+
paginated["done"] = done
|
542
|
+
paginated["totalSize"] = total_size
|
543
|
+
if "nextRecordsUrl" in paginated:
|
544
|
+
del paginated["nextRecordsUrl"]
|
545
|
+
return paginated
|
546
|
+
|
513
547
|
def query(self, query: str, tooling: bool = False) -> Optional[Dict[str, Any]]:
|
514
548
|
"""
|
515
549
|
Execute a SOQL query using the REST or Tooling API.
|
@@ -524,53 +558,29 @@ class SFAuth:
|
|
524
558
|
endpoint += query_string
|
525
559
|
headers = self._get_common_headers()
|
526
560
|
|
527
|
-
paginated_results = {"totalSize": 0, "done": False, "records": []}
|
528
|
-
|
529
561
|
try:
|
530
|
-
|
531
|
-
|
532
|
-
|
533
|
-
headers
|
534
|
-
|
535
|
-
|
536
|
-
|
537
|
-
|
538
|
-
|
562
|
+
status_code, data = self._send_request(
|
563
|
+
method="GET",
|
564
|
+
endpoint=endpoint,
|
565
|
+
headers=headers,
|
566
|
+
)
|
567
|
+
if status_code == 200:
|
568
|
+
result = json.loads(data)
|
569
|
+
paginated = self._paginate_query_result(result, headers)
|
570
|
+
logger.debug(
|
571
|
+
"Query successful, returned %s records: %r",
|
572
|
+
paginated.get("totalSize"),
|
573
|
+
query,
|
539
574
|
)
|
540
|
-
|
541
|
-
|
542
|
-
|
543
|
-
|
544
|
-
|
545
|
-
|
546
|
-
|
547
|
-
|
548
|
-
|
549
|
-
"done": query_done,
|
550
|
-
"records": paginated_results["records"],
|
551
|
-
}
|
552
|
-
logger.debug(
|
553
|
-
"Query successful, returned %s records: %r",
|
554
|
-
total_size,
|
555
|
-
query,
|
556
|
-
)
|
557
|
-
logger.trace("Query full response: %s", data)
|
558
|
-
break
|
559
|
-
endpoint = current_results.get("nextRecordsUrl")
|
560
|
-
logger.debug(
|
561
|
-
"Query batch successful, getting next batch: %s", endpoint
|
562
|
-
)
|
563
|
-
else:
|
564
|
-
logger.debug("Query failed: %r", query)
|
565
|
-
logger.error(
|
566
|
-
"Query failed with HTTP status %s",
|
567
|
-
status_code,
|
568
|
-
)
|
569
|
-
logger.debug("Query response: %s", data)
|
570
|
-
break
|
571
|
-
|
572
|
-
return paginated_results
|
573
|
-
|
575
|
+
logger.trace("Query full response: %s", paginated)
|
576
|
+
return paginated
|
577
|
+
else:
|
578
|
+
logger.debug("Query failed: %r", query)
|
579
|
+
logger.error(
|
580
|
+
"Query failed with HTTP status %s",
|
581
|
+
status_code,
|
582
|
+
)
|
583
|
+
logger.debug("Query response: %s", data)
|
574
584
|
except Exception as err:
|
575
585
|
logger.exception("Exception during query: %s", err)
|
576
586
|
|
@@ -664,7 +674,7 @@ class SFAuth:
|
|
664
674
|
logger.warning("No queries to execute.")
|
665
675
|
return None
|
666
676
|
|
667
|
-
def _execute_batch(
|
677
|
+
def _execute_batch(batch_keys, batch_queries):
|
668
678
|
endpoint = f"/services/data/{self.api_version}/composite/batch"
|
669
679
|
headers = self._get_common_headers()
|
670
680
|
|
@@ -675,7 +685,7 @@ class SFAuth:
|
|
675
685
|
"method": "GET",
|
676
686
|
"url": f"/services/data/{self.api_version}/query?q={quote(query)}",
|
677
687
|
}
|
678
|
-
for query in
|
688
|
+
for query in batch_queries
|
679
689
|
],
|
680
690
|
}
|
681
691
|
|
@@ -692,51 +702,21 @@ class SFAuth:
|
|
692
702
|
logger.trace("Composite query full response: %s", data)
|
693
703
|
results = json.loads(data).get("results", [])
|
694
704
|
for i, result in enumerate(results):
|
695
|
-
|
696
|
-
if
|
697
|
-
|
698
|
-
|
699
|
-
|
700
|
-
|
701
|
-
|
702
|
-
if next_url:
|
703
|
-
status_code, next_data = self._send_request(
|
704
|
-
method="GET",
|
705
|
-
endpoint=next_url,
|
706
|
-
headers=headers,
|
707
|
-
)
|
708
|
-
if status_code == 200:
|
709
|
-
next_results = json.loads(next_data)
|
710
|
-
records.extend(next_results.get("records", []))
|
711
|
-
result["result"]["done"] = next_results.get("done")
|
712
|
-
else:
|
713
|
-
logger.error(
|
714
|
-
"Failed to fetch next records: %s",
|
715
|
-
next_data,
|
716
|
-
)
|
717
|
-
break
|
718
|
-
else:
|
719
|
-
result["result"]["done"] = True
|
720
|
-
paginated_results = result["result"]
|
721
|
-
paginated_results["records"] = records
|
722
|
-
if "nextRecordsUrl" in paginated_results:
|
723
|
-
del paginated_results["nextRecordsUrl"]
|
724
|
-
batch_results[keys[i]] = paginated_results
|
725
|
-
if result.get("statusCode") != 200:
|
726
|
-
logger.error("Query failed for key %s: %s", keys[i], result)
|
727
|
-
logger.error(
|
728
|
-
"Query failed with HTTP status %s (%s)",
|
729
|
-
result.get("statusCode"),
|
730
|
-
result.get("statusMessage"),
|
731
|
-
)
|
732
|
-
logger.trace("Query response: %s", result)
|
705
|
+
key = batch_keys[i]
|
706
|
+
if result.get("statusCode") == 200 and "result" in result:
|
707
|
+
paginated = self._paginate_query_result(result["result"], headers)
|
708
|
+
batch_results[key] = paginated
|
709
|
+
else:
|
710
|
+
logger.error("Query failed for key %s: %s", key, result)
|
711
|
+
batch_results[key] = result
|
733
712
|
else:
|
734
713
|
logger.error(
|
735
714
|
"Composite query failed with HTTP status %s (%s)",
|
736
715
|
status_code,
|
737
716
|
data,
|
738
717
|
)
|
739
|
-
|
718
|
+
for i, key in enumerate(batch_keys):
|
719
|
+
batch_results[key] = data
|
740
720
|
logger.trace("Composite query response: %s", data)
|
741
721
|
|
742
722
|
return batch_results
|
@@ -746,11 +726,11 @@ class SFAuth:
|
|
746
726
|
|
747
727
|
with ThreadPoolExecutor(max_workers=max_workers) as executor:
|
748
728
|
futures = []
|
749
|
-
BATCH_SIZE =
|
729
|
+
BATCH_SIZE = batch_size
|
750
730
|
for i in range(0, len(keys), BATCH_SIZE):
|
751
731
|
batch_keys = keys[i : i + BATCH_SIZE]
|
752
732
|
batch_queries = [query_dict[key] for key in batch_keys]
|
753
|
-
futures.append(executor.submit(_execute_batch, batch_queries))
|
733
|
+
futures.append(executor.submit(_execute_batch, batch_keys, batch_queries))
|
754
734
|
|
755
735
|
for future in as_completed(futures):
|
756
736
|
results_dict.update(future.result())
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|