sfq 0.0.16__py3-none-any.whl → 0.0.17__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
@@ -84,9 +84,9 @@ class SFAuth:
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.16",
87
+ user_agent: str = "sfq/0.0.17",
88
88
  sforce_client: str = "_auto",
89
- proxy: str = "auto",
89
+ proxy: str = "_auto",
90
90
  ) -> None:
91
91
  """
92
92
  Initializes the SFAuth with necessary parameters.
@@ -100,9 +100,9 @@ class SFAuth:
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.16").
103
+ :param user_agent: Custom User-Agent string (default is "sfq/0.0.17").
104
104
  :param sforce_client: Custom Application Identifier (default is user_agent).
105
- :param proxy: The proxy configuration, "auto" to use environment (default is "auto").
105
+ :param proxy: The proxy configuration, "_auto" to use environment (default is "_auto").
106
106
  """
107
107
  self.instance_url = self._format_instance_url(instance_url)
108
108
  self.client_id = client_id
@@ -114,12 +114,12 @@ class SFAuth:
114
114
  self.token_expiration_time = token_expiration_time
115
115
  self.token_lifetime = token_lifetime
116
116
  self.user_agent = user_agent
117
- self.sforce_client = quote(str(sforce_client), safe="")
117
+ self.sforce_client = str(sforce_client).replace(",", "")
118
118
  self._auto_configure_proxy(proxy)
119
119
  self._high_api_usage_threshold = 80
120
120
 
121
121
  if sforce_client == "_auto":
122
- self.sforce_client = quote(str(user_agent), safe="")
122
+ self.sforce_client = user_agent
123
123
 
124
124
  if self.client_secret == "_deprecation_warning":
125
125
  warnings.warn(
@@ -134,6 +134,13 @@ class SFAuth:
134
134
  )
135
135
 
136
136
  def _format_instance_url(self, instance_url) -> str:
137
+ """
138
+ HTTPS is mandatory with Spring '21 release,
139
+ This method ensures that the instance URL is formatted correctly.
140
+
141
+ :param instance_url: The Salesforce instance URL.
142
+ :return: The formatted instance URL.
143
+ """
137
144
  if instance_url.startswith("https://"):
138
145
  return instance_url
139
146
  if instance_url.startswith("http://"):
@@ -144,8 +151,8 @@ class SFAuth:
144
151
  """
145
152
  Automatically configure the proxy based on the environment or provided value.
146
153
  """
147
- if proxy == "auto":
148
- self.proxy = os.environ.get("https_proxy")
154
+ if proxy == "_auto":
155
+ self.proxy = os.environ.get("https_proxy") # HTTPs is mandatory
149
156
  if self.proxy:
150
157
  logger.debug("Auto-configured proxy: %s", self.proxy)
151
158
  else:
@@ -798,3 +805,74 @@ class SFAuth:
798
805
  if isinstance(result, (dict, list))
799
806
  ]
800
807
  return combined_response or None
808
+
809
+ def _cupdate(self, update_dict: Dict[str, Any], batch_size: int = 25, max_workers: int = None) -> Optional[Dict[str, Any]]:
810
+ """
811
+ Execute the Composite Update API to update multiple records.
812
+
813
+ :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. Example:
814
+ {'001aj00000C8kJhAAJ': {'Subject': 'Easily updated via SFQ'}, '00aaj000006wtdcAAA': {'_': 'CaseComment', 'IsPublished': False}, '001aj0000002yJRCAY': {'_': 'IdeaComment', 'CommentBody': 'Hello World!'}}
815
+ :param batch_size: The number of records to update in each batch (default is 25).
816
+ :return: JSON response from the update request or None on failure.
817
+ """
818
+ allOrNone = False
819
+ endpoint = f"/services/data/{self.api_version}/composite"
820
+
821
+ compositeRequest_payload = []
822
+ sobject_prefixes = {}
823
+
824
+ for key, record in update_dict.items():
825
+ sobject = record.copy().pop("_", None)
826
+ if not sobject and not sobject_prefixes:
827
+ sobject_prefixes = self.get_sobject_prefixes()
828
+
829
+ sobject = str(sobject) or str(sobject_prefixes.get(str(key[:3]), None))
830
+
831
+ compositeRequest_payload.append(
832
+ {
833
+ 'method': 'PATCH',
834
+ 'url': f"/services/data/{self.api_version}/sobjects/{sobject}/{key}",
835
+ 'referenceId': key,
836
+ 'body': record,
837
+ }
838
+ )
839
+
840
+ chunks = [compositeRequest_payload[i:i+batch_size] for i in range(0, len(compositeRequest_payload), batch_size)]
841
+
842
+ def update_chunk(chunk: List[Dict[str, Any]]) -> Optional[Dict[str, Any]]:
843
+ payload = {
844
+ "allOrNone": bool(allOrNone),
845
+ "compositeRequest": chunk
846
+ }
847
+
848
+ status_code, resp_data = self._send_request(
849
+ method="POST",
850
+ endpoint=endpoint,
851
+ headers=self._get_common_headers(),
852
+ body=json.dumps(payload),
853
+ )
854
+
855
+ if status_code == 200:
856
+ logger.debug("Composite update API response without errors.")
857
+ return json.loads(resp_data)
858
+ else:
859
+ logger.error("Composite update API request failed: %s", status_code)
860
+ logger.debug("Response body: %s", resp_data)
861
+ return None
862
+
863
+ results = []
864
+ with ThreadPoolExecutor(max_workers=max_workers) as executor:
865
+ futures = [executor.submit(update_chunk, chunk) for chunk in chunks]
866
+ for future in as_completed(futures):
867
+ result = future.result()
868
+ if result:
869
+ results.append(result)
870
+
871
+ combined_response = [
872
+ item
873
+ for result in results
874
+ for item in (result if isinstance(result, list) else [result])
875
+ if isinstance(result, (dict, list))
876
+ ]
877
+
878
+ return combined_response or None
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sfq
3
- Version: 0.0.16
3
+ Version: 0.0.17
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
@@ -56,18 +56,6 @@ print(sf.query("SELECT Id FROM Account LIMIT 5"))
56
56
  print(sf.tooling_query("SELECT Id, FullName, Metadata FROM SandboxSettings LIMIT 5"))
57
57
  ```
58
58
 
59
- ### sObject Key Prefixes
60
-
61
- ```python
62
- # Key prefix via IDs
63
- print(sf.get_sobject_prefixes())
64
- >>> {'0Pp': 'AIApplication', '6S9': 'AIApplicationConfig', '9qd': 'AIInsightAction', '9bq': 'AIInsightFeedback', '0T2': 'AIInsightReason', '9qc': 'AIInsightValue', ...}
65
-
66
- # Key prefix via names
67
- print(sf.get_sobject_prefixes(key_type="name"))
68
- >>> {'AIApplication': '0Pp', 'AIApplicationConfig': '6S9', 'AIInsightAction': '9qd', 'AIInsightFeedback': '9bq', 'AIInsightReason': '0T2', 'AIInsightValue': '9qc', ...}
69
- ```
70
-
71
59
  ### Composite Batch Queries
72
60
 
73
61
  ```python
@@ -94,7 +82,7 @@ for subrequest_identifer, subrequest_response in batched_response.items():
94
82
 
95
83
  ```python
96
84
  response = sf.cdelete(['07La0000000bYgj', '07La0000000bYgk', '07La0000000bYgl'])
97
- >>> [{'id': '500aj000006wtdZAAQ', 'success': True, 'errors': []}, {'id': '500aj000006wtdaAAA', 'success': True, 'errors': []}, {'id': '500aj000006wtdbAAA', 'success': True, 'errors': []}]
85
+ >>> [{'id': '07La0000000bYgj', 'success': True, 'errors': []}, {'id': '07La0000000bYgk', 'success': True, 'errors': []}, {'id': '07La0000000bYgl', 'success': True, 'errors': []}]
98
86
  ```
99
87
 
100
88
  ### Static Resources
@@ -110,6 +98,18 @@ print(f'Updated resource: {page}')
110
98
  sf.update_static_resource_id('081aj000009jUMXAA2', '<h1>It works!</h1>')
111
99
  ```
112
100
 
101
+ ### sObject Key Prefixes
102
+
103
+ ```python
104
+ # Key prefix via IDs
105
+ print(sf.get_sobject_prefixes())
106
+ >>> {'0Pp': 'AIApplication', '6S9': 'AIApplicationConfig', '9qd': 'AIInsightAction', '9bq': 'AIInsightFeedback', '0T2': 'AIInsightReason', '9qc': 'AIInsightValue', ...}
107
+
108
+ # Key prefix via names
109
+ print(sf.get_sobject_prefixes(key_type="name"))
110
+ >>> {'AIApplication': '0Pp', 'AIApplicationConfig': '6S9', 'AIInsightAction': '9qd', 'AIInsightFeedback': '9bq', 'AIInsightReason': '0T2', 'AIInsightValue': '9qc', ...}
111
+ ```
112
+
113
113
  ## How to Obtain Salesforce Tokens
114
114
 
115
115
  To use the `sfq` library, you'll need a **client ID** and **refresh token**. The easiest way to obtain these is by using the Salesforce CLI:
@@ -0,0 +1,6 @@
1
+ sfq/__init__.py,sha256=qtXrbjzTQL1nU2RaMbxWUDRFt4CAUKAkwwp-_9lqavE,35361
2
+ sfq/_cometd.py,sha256=XimQEubmJwUmbWe85TxH_cuhGvWVuiHHrVr41tguuiI,10508
3
+ sfq/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
+ sfq-0.0.17.dist-info/METADATA,sha256=XgKcuglHsZ1dN1rqNSgaWb-A-wf8-bStAMt2Gmj4GlE,6899
5
+ sfq-0.0.17.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
6
+ sfq-0.0.17.dist-info/RECORD,,
@@ -1,6 +0,0 @@
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,,
File without changes