sfq 0.0.7__py3-none-any.whl → 0.0.8__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
@@ -1,38 +1,91 @@
1
1
  import http.client
2
+ import json
2
3
  import logging
3
- import time
4
4
  import os
5
- import json
6
- from urllib.parse import urlparse, quote
5
+ import time
6
+ from typing import Any, Dict, Optional
7
+ from urllib.parse import quote, urlparse
8
+
9
+ TRACE = 5
10
+ logging.addLevelName(TRACE, "TRACE")
11
+
12
+
13
+ def trace(self: logging.Logger, message: str, *args: Any, **kwargs: Any) -> None:
14
+ """Custom TRACE level logging function with redaction."""
15
+
16
+ def _redact_sensitive(data: Any) -> Any:
17
+ """Redacts sensitive keys from a dictionary or query string."""
18
+ REDACT_VALUE = "*" * 8
19
+ if isinstance(data, dict):
20
+ return {
21
+ k: (
22
+ REDACT_VALUE
23
+ if k.lower() in ["access_token", "authorization", "refresh_token"]
24
+ else v
25
+ )
26
+ for k, v in data.items()
27
+ }
28
+ elif isinstance(data, str):
29
+ parts = data.split("&")
30
+ for i, part in enumerate(parts):
31
+ if "=" in part:
32
+ key, value = part.split("=", 1)
33
+ if key.lower() in [
34
+ "access_token",
35
+ "authorization",
36
+ "refresh_token",
37
+ ]:
38
+ parts[i] = f"{key}={REDACT_VALUE}"
39
+ return "&".join(parts)
40
+ return data
41
+
42
+ redacted_args = args
43
+ if args:
44
+ first = args[0]
45
+ if isinstance(first, str):
46
+ try:
47
+ loaded = json.loads(first)
48
+ first = loaded
49
+ except (json.JSONDecodeError, TypeError):
50
+ pass
51
+ redacted_first = _redact_sensitive(first)
52
+ redacted_args = (redacted_first,) + args[1:]
53
+
54
+ if self.isEnabledFor(TRACE):
55
+ self._log(TRACE, message, redacted_args, **kwargs)
56
+
57
+
58
+ logging.Logger.trace = trace
59
+ logger = logging.getLogger("sfq")
7
60
 
8
- logging.basicConfig(level=logging.INFO, format='[sfq:%(lineno)d - %(levelname)s] %(message)s')
9
61
 
10
62
  class SFAuth:
11
63
  def __init__(
12
64
  self,
13
- instance_url,
14
- client_id,
15
- refresh_token,
16
- api_version="v63.0",
17
- token_endpoint="/services/oauth2/token",
18
- access_token=None,
19
- token_expiration_time=None,
20
- token_lifetime=15 * 60,
21
- proxy="auto",
22
- user_agent="sfq/0.0.7",
23
- ):
65
+ instance_url: str,
66
+ client_id: str,
67
+ refresh_token: str,
68
+ api_version: str = "v63.0",
69
+ token_endpoint: str = "/services/oauth2/token",
70
+ access_token: Optional[str] = None,
71
+ token_expiration_time: Optional[float] = None,
72
+ token_lifetime: int = 15 * 60,
73
+ user_agent: str = "sfq/0.0.8",
74
+ proxy: str = "auto",
75
+ ) -> None:
24
76
  """
25
77
  Initializes the SFAuth with necessary parameters.
26
78
 
27
79
  :param instance_url: The Salesforce instance URL.
28
80
  :param client_id: The client ID for OAuth.
29
81
  :param refresh_token: The refresh token for OAuth.
30
- :param api_version: The Salesforce API version (default is "v62.0").
82
+ :param api_version: The Salesforce API version (default is "v63.0").
31
83
  :param token_endpoint: The token endpoint (default is "/services/oauth2/token").
32
84
  :param access_token: The access token for the current session (default is None).
33
85
  :param token_expiration_time: The expiration time of the access token (default is None).
34
- :param token_lifetime: The lifetime of the access token (default is 15 minutes).
35
- :param proxy: The proxy configuration (default is "auto").
86
+ :param token_lifetime: The lifetime of the access token in seconds (default is 15 minutes).
87
+ :param user_agent: Custom User-Agent string (default is "sfq/0.0.8").
88
+ :param proxy: The proxy configuration, "auto" to use environment (default is "auto").
36
89
  """
37
90
  self.instance_url = instance_url
38
91
  self.client_id = client_id
@@ -42,132 +95,307 @@ class SFAuth:
42
95
  self.access_token = access_token
43
96
  self.token_expiration_time = token_expiration_time
44
97
  self.token_lifetime = token_lifetime
45
- self._auto_configure_proxy(proxy)
46
98
  self.user_agent = user_agent
99
+ self._auto_configure_proxy(proxy)
100
+ self._high_api_usage_threshold = 80
47
101
 
48
- def _auto_configure_proxy(self, proxy):
102
+ def _auto_configure_proxy(self, proxy: str) -> None:
49
103
  """
50
- Automatically configure the proxy based on the environment.
104
+ Automatically configure the proxy based on the environment or provided value.
51
105
  """
52
106
  if proxy == "auto":
53
- if "https_proxy" in os.environ:
54
- self.proxy = os.environ["https_proxy"]
55
- else:
56
- self.proxy = None
107
+ self.proxy = os.environ.get("https_proxy")
108
+ if self.proxy:
109
+ logger.debug("Auto-configured proxy: %s", self.proxy)
57
110
  else:
58
111
  self.proxy = proxy
112
+ logger.debug("Using configured proxy: %s", self.proxy)
59
113
 
60
- def _prepare_payload(self):
61
- """Prepare the payload for the token request."""
114
+ def _prepare_payload(self) -> Dict[str, str]:
115
+ """
116
+ Prepare the payload for the token request.
117
+ """
62
118
  return {
63
119
  "grant_type": "refresh_token",
64
120
  "client_id": self.client_id,
65
121
  "refresh_token": self.refresh_token,
66
122
  }
67
123
 
68
- def _create_connection(self, netloc):
124
+ def _create_connection(self, netloc: str) -> http.client.HTTPConnection:
69
125
  """
70
- Create a connection using HTTP or HTTPS, depending on the proxy configuration.
126
+ Create a connection using HTTP or HTTPS, with optional proxy support.
127
+
128
+ :param netloc: The target host and port from the parsed instance URL.
129
+ :return: An HTTP(S)Connection object.
71
130
  """
72
131
  if self.proxy:
73
132
  proxy_url = urlparse(self.proxy)
74
- if proxy_url.scheme == "http://":
75
- conn = http.client.HTTPConnection(proxy_url.hostname, proxy_url.port)
76
- else:
77
- conn = http.client.HTTPSConnection(proxy_url.hostname, proxy_url.port)
133
+ conn_cls = (
134
+ http.client.HTTPSConnection
135
+ if proxy_url.scheme != "http"
136
+ else http.client.HTTPConnection
137
+ )
138
+ conn = conn_cls(proxy_url.hostname, proxy_url.port)
78
139
  conn.set_tunnel(netloc)
140
+ logger.trace("Using proxy tunnel to %s", netloc)
79
141
  else:
80
142
  conn = http.client.HTTPSConnection(netloc)
143
+ logger.trace("Direct connection to %s", netloc)
81
144
  return conn
82
145
 
83
- def _send_post_request(self, payload):
84
- """Send a POST request to the Salesforce token endpoint using http.client."""
146
+ def _post_token_request(self, payload: Dict[str, str]) -> Optional[Dict[str, Any]]:
147
+ """
148
+ Send a POST request to the Salesforce token endpoint using http.client.
149
+
150
+ :param payload: Dictionary of form-encoded OAuth parameters.
151
+ :return: Parsed JSON response if successful, otherwise None.
152
+ """
85
153
  parsed_url = urlparse(self.instance_url)
86
154
  conn = self._create_connection(parsed_url.netloc)
87
-
88
- headers = {"Content-Type": "application/x-www-form-urlencoded", "User-Agent": self.user_agent}
89
- body = "&".join([f"{key}={quote(str(value))}" for key, value in payload.items()])
155
+ headers = {
156
+ "Accept": "application/json",
157
+ "Content-Type": "application/x-www-form-urlencoded",
158
+ "User-Agent": self.user_agent,
159
+ }
160
+ body = "&".join(f"{key}={quote(str(value))}" for key, value in payload.items())
90
161
 
91
162
  try:
163
+ logger.trace("Request endpoint: %s", self.token_endpoint)
164
+ logger.trace("Request body: %s", body)
165
+ logger.trace("Request headers: %s", headers)
92
166
  conn.request("POST", self.token_endpoint, body, headers)
93
167
  response = conn.getresponse()
94
168
  data = response.read().decode("utf-8")
169
+ self._http_resp_header_logic(response)
170
+
95
171
  if response.status == 200:
172
+ logger.trace("Token refresh successful.")
173
+ logger.trace("Response body: %s", data)
96
174
  return json.loads(data)
97
- else:
98
- logging.error(
99
- f"HTTP error occurred: {response.status} {response.reason}"
100
- )
101
- logging.error(f"Response content: {data}")
175
+
176
+ logger.error(
177
+ "Token refresh failed: %s %s", response.status, response.reason
178
+ )
179
+ logger.debug("Response body: %s", data)
180
+
102
181
  except Exception as err:
103
- logging.error(f"Other error occurred: {err}")
182
+ logger.exception("Error during token request: %s", err)
183
+
104
184
  finally:
105
185
  conn.close()
106
186
 
107
187
  return None
108
188
 
109
- def _refresh_token_if_needed(self):
110
- """Automatically refresh the token if it has expired or is missing."""
111
- _token_expiration = self._is_token_expired()
112
- if self.access_token and not _token_expiration:
189
+ def _http_resp_header_logic(self, response: http.client.HTTPResponse) -> None:
190
+ """
191
+ Perform additional logic based on the HTTP response headers.
192
+
193
+ :param response: The HTTP response object.
194
+ :return: None
195
+ """
196
+ logger.trace(
197
+ "Response status: %s, reason: %s", response.status, response.reason
198
+ )
199
+ headers = response.getheaders()
200
+ headers_list = [(k, v) for k, v in headers if not v.startswith("BrowserId=")]
201
+ logger.trace("Response headers: %s", headers_list)
202
+ for key, value in headers_list:
203
+ if key.startswith("Sforce-"):
204
+ if key == "Sforce-Limit-Info":
205
+ current_api_calls = int(value.split("=")[1].split("/")[0])
206
+ maximum_api_calls = int(value.split("=")[1].split("/")[1])
207
+ usage_percentage = round(
208
+ current_api_calls / maximum_api_calls * 100, 2
209
+ )
210
+ if usage_percentage > self._high_api_usage_threshold:
211
+ logger.warning(
212
+ "High API usage: %s/%s (%s%%)",
213
+ current_api_calls,
214
+ maximum_api_calls,
215
+ usage_percentage,
216
+ )
217
+ else:
218
+ logger.debug(
219
+ "API usage: %s/%s (%s%%)",
220
+ current_api_calls,
221
+ maximum_api_calls,
222
+ usage_percentage,
223
+ )
224
+
225
+ def _refresh_token_if_needed(self) -> Optional[str]:
226
+ """
227
+ Automatically refresh the access token if it has expired or is missing.
228
+
229
+ :return: A valid access token or None if refresh failed.
230
+ """
231
+ if self.access_token and not self._is_token_expired():
113
232
  return self.access_token
114
-
115
- if not self.access_token:
116
- logging.debug("No access token available. Requesting a new one.")
117
- elif _token_expiration:
118
- logging.debug("Access token has expired. Requesting a new one.")
119
233
 
234
+ logger.trace("Access token expired or missing, refreshing...")
120
235
  payload = self._prepare_payload()
121
- token_data = self._send_post_request(payload)
236
+ token_data = self._post_token_request(payload)
237
+
122
238
  if token_data:
123
- self.access_token = token_data["access_token"]
124
- self.token_expiration_time = int(token_data["issued_at"]) + int(self.token_lifetime)
125
- logging.debug("Access token refreshed successfully.")
126
- else:
127
- logging.error("Failed to refresh access token.")
128
- return self.access_token
239
+ self.access_token = token_data.get("access_token")
240
+ issued_at = token_data.get("issued_at")
241
+
242
+ try:
243
+ self.org_id = token_data.get("id").split("/")[4]
244
+ self.user_id = token_data.get("id").split("/")[5]
245
+ logger.trace(
246
+ "Authenticated as user %s in org %s", self.user_id, self.org_id
247
+ )
248
+ except (IndexError, KeyError):
249
+ logger.error("Failed to extract org/user IDs from token response.")
129
250
 
251
+ if self.access_token and issued_at:
252
+ self.token_expiration_time = int(issued_at) + self.token_lifetime
253
+ logger.trace("New token expires at %s", self.token_expiration_time)
254
+ return self.access_token
255
+
256
+ logger.error("Failed to obtain access token.")
257
+ return None
130
258
 
131
- def _is_token_expired(self):
132
- """Check if the access token has expired."""
259
+ def _is_token_expired(self) -> bool:
260
+ """
261
+ Check if the access token has expired.
262
+
263
+ :return: True if token is expired or missing, False otherwise.
264
+ """
133
265
  try:
134
266
  return time.time() >= float(self.token_expiration_time)
135
267
  except (TypeError, ValueError):
268
+ logger.warning("Token expiration check failed. Treating token as expired.")
136
269
  return True
137
270
 
138
- def query(self, query, tooling=False):
139
- """Query Salesforce using SOQL or Tooling API, depending on the `tooling` parameter."""
271
+ def tooling_query(self, query: str) -> Optional[Dict[str, Any]]:
272
+ """
273
+ Execute a SOQL query using the Tooling API.
274
+
275
+ :param query: The SOQL query string.
276
+ :return: Parsed JSON response or None on failure.
277
+ """
278
+ return self.query(query, tooling=True)
279
+
280
+ def limits(self) -> Optional[Dict[str, Any]]:
140
281
  self._refresh_token_if_needed()
141
282
 
142
283
  if not self.access_token:
143
- logging.error("No access token available to make the query.")
284
+ logger.error("No access token available for limits.")
144
285
  return None
145
286
 
146
- if tooling:
147
- query_endpoint = f"/services/data/{self.api_version}/tooling/query"
148
- else:
149
- query_endpoint = f"/services/data/{self.api_version}/query"
150
-
151
- headers = {"Authorization": f"Bearer {self.access_token}", "User-Agent": self.user_agent}
152
-
153
- # Handle special characters in the query
154
- encoded_query = quote(query)
155
- params = f"?q={encoded_query}"
287
+ endpoint = f"/services/data/{self.api_version}/limits"
288
+ headers = {
289
+ "Authorization": f"Bearer {self.access_token}",
290
+ "User-Agent": self.user_agent,
291
+ "Content-Type": "application/x-www-form-urlencoded",
292
+ "Accept": "application/json",
293
+ }
156
294
 
157
295
  parsed_url = urlparse(self.instance_url)
158
296
  conn = self._create_connection(parsed_url.netloc)
159
297
 
160
298
  try:
161
- conn.request("GET", query_endpoint + params, headers=headers)
299
+ logger.trace("Request endpoint: %s", endpoint)
300
+ logger.trace("Request headers: %s", headers)
301
+ conn.request("GET", endpoint, headers=headers)
162
302
  response = conn.getresponse()
163
303
  data = response.read().decode("utf-8")
304
+ self._http_resp_header_logic(response)
305
+
164
306
  if response.status == 200:
307
+ logger.debug("Limits API request successful.")
308
+ logger.trace("Response body: %s", data)
165
309
  return json.loads(data)
166
- else:
167
- logging.error(f"HTTP error occurred during query: {response.status} {response.reason}")
168
- logging.error(f"Response content: {data}")
310
+
311
+ logger.error("Limits API request failed: %s %s", response.status, response.reason)
312
+ logger.debug("Response body: %s", data)
313
+
169
314
  except Exception as err:
170
- logging.error(f"Other error occurred during query: {err}")
315
+ logger.exception("Error during limits request: %s", err)
316
+
317
+ finally:
318
+ conn.close()
319
+
320
+ return None
321
+
322
+ def query(self, query: str, tooling: bool = False) -> Optional[Dict[str, Any]]:
323
+ """
324
+ Execute a SOQL query using the REST or Tooling API.
325
+
326
+ :param query: The SOQL query string.
327
+ :param tooling: If True, use the Tooling API endpoint.
328
+ :return: Parsed JSON response or None on failure.
329
+ """
330
+ self._refresh_token_if_needed()
331
+
332
+ if not self.access_token:
333
+ logger.error("No access token available for query.")
334
+ return None
335
+
336
+ endpoint = f"/services/data/{self.api_version}/"
337
+ endpoint += "tooling/query" if tooling else "query"
338
+ query_string = f"?q={quote(query)}"
339
+
340
+ endpoint += query_string
341
+
342
+ headers = {
343
+ "Authorization": f"Bearer {self.access_token}",
344
+ "User-Agent": self.user_agent,
345
+ "Content-Type": "application/x-www-form-urlencoded",
346
+ "Accept": "application/json",
347
+ }
348
+
349
+ parsed_url = urlparse(self.instance_url)
350
+ conn = self._create_connection(parsed_url.netloc)
351
+
352
+ try:
353
+ paginated_results = {"totalSize": 0, "done": False, "records": []}
354
+ while True:
355
+ logger.trace("Request endpoint: %s", endpoint)
356
+ logger.trace("Request headers: %s", headers)
357
+ conn.request("GET", endpoint, headers=headers)
358
+ response = conn.getresponse()
359
+ data = response.read().decode("utf-8")
360
+ self._http_resp_header_logic(response)
361
+
362
+ if response.status == 200:
363
+ current_results = json.loads(data)
364
+ paginated_results["records"].extend(current_results["records"])
365
+ query_done = current_results.get("done")
366
+ if query_done:
367
+ total_size = current_results.get("totalSize")
368
+ paginated_results = {
369
+ "totalSize": total_size,
370
+ "done": query_done,
371
+ "records": paginated_results["records"],
372
+ }
373
+ logger.debug(
374
+ "Query successful, returned %s records: %r",
375
+ total_size,
376
+ query,
377
+ )
378
+ logger.trace("Query full response: %s", data)
379
+ break
380
+ endpoint = current_results.get("nextRecordsUrl")
381
+ logger.debug(
382
+ "Query batch successful, getting next batch: %s", endpoint
383
+ )
384
+ else:
385
+ logger.debug("Query failed: %r", query)
386
+ logger.error(
387
+ "Query failed with HTTP status %s (%s)",
388
+ response.status,
389
+ response.reason,
390
+ )
391
+ logger.debug("Query response: %s", data)
392
+ break
393
+
394
+ return paginated_results
395
+
396
+ except Exception as err:
397
+ logger.exception("Exception during query: %s", err)
398
+
171
399
  finally:
172
400
  conn.close()
173
401
 
@@ -1,20 +1,17 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: sfq
3
- Version: 0.0.7
3
+ Version: 0.0.8
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
7
7
  Classifier: Development Status :: 3 - Alpha
8
8
  Classifier: Intended Audience :: Developers
9
- Classifier: Programming Language :: Python :: 3.7
10
- Classifier: Programming Language :: Python :: 3.8
11
9
  Classifier: Programming Language :: Python :: 3.9
12
10
  Classifier: Programming Language :: Python :: 3.10
13
11
  Classifier: Programming Language :: Python :: 3.12
14
12
  Classifier: Programming Language :: Python :: 3.13
15
13
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
16
- Requires-Python: >=3.7
17
- Requires-Dist: prompt-toolkit>=3.0.3
14
+ Requires-Python: >=3.9
18
15
  Description-Content-Type: text/markdown
19
16
 
20
17
  # sfq (Salesforce Query)
@@ -39,24 +36,6 @@ pip install sfq
39
36
 
40
37
  ## Usage
41
38
 
42
- ### Interactive Querying
43
-
44
- ```powershell
45
- usage: python -m sfq [-a SFDXAUTHURL] [--dry-run] [--disable-fuzzy-completion]
46
-
47
- Interactively query Salesforce data with real-time autocompletion.
48
-
49
- options:
50
- -h, --help show this help message and exit
51
- -a, --sfdxAuthUrl SFDXAUTHURL
52
- Salesforce auth url
53
- --dry-run Print the query without executing it
54
- --disable-fuzzy-completion
55
- Disable fuzzy completion
56
- ```
57
-
58
- You can run the `sfq` library in interactive mode by passing the `-a` option with the `SFDX_AUTH_URL` argument or by setting the `SFDX_AUTH_URL` environment variable.
59
-
60
39
  ### Library Querying
61
40
 
62
41
  ```python
@@ -146,7 +125,9 @@ To use the `sfq` library, you'll need a **client ID** and **refresh token**. The
146
125
  pip install sfq && python -c "from sfq import SFAuth; sf = SFAuth(instance_url='$instanceUrl', client_id='$clientId', refresh_token='$refreshToken'); print(sf.query('$query'))" | jq -r '.records[].Id'
147
126
  ```
148
127
 
149
- ## Notes
128
+ ## Important Considerations
129
+
130
+ - **Security**: Safeguard your refresh token diligently, as it provides access to your Salesforce environment. Avoid sharing or exposing it in unsecured locations.
131
+ - **Efficient Data Retrieval**: The `query` function automatically handles pagination, simplifying record retrieval across large datasets. It's recommended to use the `LIMIT` clause in queries to control the volume of data returned.
132
+ - **Advanced Metadata Queries**: Utilize the `tooling=True` option within the `query` function to access the Salesforce Tooling API. This option is designed for performing complex metadata operations, enhancing your data management capabilities.
150
133
 
151
- - **Authentication**: Make sure your refresh token is kept secure, as it grants access to your Salesforce instance.
152
- - **Tooling API**: You can set the `tooling=True` argument in the `query` method to access the Salesforce Tooling API for more advanced metadata queries. This is limited to library usage only.
@@ -0,0 +1,5 @@
1
+ sfq/__init__.py,sha256=MlnlaJCHz4NgL_nudfa15Aman7LdgEtv-hY3G-M731E,15203
2
+ sfq/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
+ sfq-0.0.8.dist-info/METADATA,sha256=I1iwzWgydgwKKslNoZvc5y0VjPKytm1R5B1wLXm7VoY,5066
4
+ sfq-0.0.8.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
5
+ sfq-0.0.8.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: hatchling 1.17.1
2
+ Generator: hatchling 1.27.0
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
sfq/__main__.py DELETED
@@ -1,151 +0,0 @@
1
- import difflib
2
- import http.client
3
- import json
4
-
5
- from sfq import SFAuth
6
-
7
- from prompt_toolkit import prompt
8
- from prompt_toolkit.completion import Completer, Completion
9
-
10
- def _interactive_shell(sf: SFAuth, dry_run: bool, disable_fuzzy_completion: bool):
11
- """Runs an interactive REPL for querying Salesforce data with real-time autocompletion."""
12
-
13
- sobject = None
14
- fields = None
15
-
16
- class DynamicSeparatorCompleter(Completer):
17
- """Custom completer that adapts to different separators."""
18
-
19
- def __init__(self, words: list[str], separators: list[str] = [","]):
20
- self.words = words
21
- self.separators = separators
22
-
23
- def get_completions(self, document, complete_event):
24
- text_before_cursor = document.text_before_cursor
25
-
26
- for separator in self.separators:
27
- if separator in text_before_cursor:
28
- last_token = text_before_cursor.split(separator)[-1].strip()
29
- break
30
- else:
31
- last_token = text_before_cursor.strip()
32
-
33
-
34
- matches_difflib = difflib.get_close_matches(last_token, self.words, n=20, cutoff=0.6)
35
- matches_starting = [word for word in self.words if word.lower().startswith(last_token.lower())]
36
-
37
- if not disable_fuzzy_completion and (len(last_token) > 6 or not(matches_starting)):
38
- matches = matches_difflib
39
- else:
40
- matches = matches_starting
41
-
42
- for word in matches:
43
- yield Completion(word, start_position=-len(last_token))
44
-
45
- def _get_objects(sf: SFAuth):
46
- """Retrieve available Salesforce objects."""
47
- host = sf.instance_url.split("://")[1].split("/")[0]
48
- conn = http.client.HTTPSConnection(host)
49
- uri = f"/services/data/{sf.api_version}/sobjects/"
50
- headers = {'Authorization': f'Bearer {sf._refresh_token_if_needed()}'}
51
- conn.request("GET", uri, headers=headers)
52
- response = conn.getresponse()
53
-
54
- if response.status != 200:
55
- print(f'Error: {response.status} {response.reason}')
56
- return []
57
-
58
- data = json.loads(response.read())
59
- return [sobject['name'] for sobject in data['sobjects']]
60
-
61
- def _get_fields(sobject: str, sf: SFAuth):
62
- """Retrieve available fields for a given Salesforce object."""
63
- host = sf.instance_url.split("://")[1].split("/")[0]
64
- conn = http.client.HTTPSConnection(host)
65
- uri = f"/services/data/{sf.api_version}/sobjects/{sobject}/describe/"
66
- headers = {'Authorization': f'Bearer {sf._refresh_token_if_needed()}'}
67
- conn.request("GET", uri, headers=headers)
68
- response = conn.getresponse()
69
-
70
- if response.status != 200:
71
- print(f'Error: {response.status} {response.reason}')
72
- raise ValueError(f'Unable to fetch fields for sObject "{sobject}": {response.status}, {response.reason}')
73
-
74
- data = json.loads(response.read())
75
- return [f['name'] for f in data['fields']]
76
-
77
- available_objects = _get_objects(sf)
78
-
79
- object_completer = DynamicSeparatorCompleter(available_objects)
80
- while not sobject:
81
- sobject = prompt('FROM ', completer=object_completer).strip()
82
-
83
-
84
- available_fields = _get_fields(sobject, sf)
85
- field_completer = DynamicSeparatorCompleter(available_fields, separators=[","])
86
- while not fields:
87
- fields = prompt('SELECT ', completer=field_completer).strip()
88
-
89
- where_completer = DynamicSeparatorCompleter(available_fields, separators=[" AND ", " OR "])
90
- where = prompt("WHERE ", completer=where_completer).strip()
91
- where_clause = f"WHERE {where}" if where else ""
92
-
93
- limit = prompt("LIMIT ", default="200").strip()
94
- limit_clause = f"LIMIT {limit}" if limit else ""
95
-
96
- query = f"SELECT {fields} FROM {sobject} {where_clause} {limit_clause}".replace(' ', ' ')
97
-
98
- if dry_run:
99
- print('\nDry-run, skipping execution...')
100
- print(f'\nQuery: {query}\n')
101
- return query
102
-
103
- print('\nExecuting query...\n')
104
- data = sf.query(query)
105
- print(json.dumps(data, indent=4))
106
- print(f'\nQuery: {query}\n')
107
- return data
108
-
109
- if __name__ == "__main__":
110
- import argparse
111
- import os
112
-
113
- parser = argparse.ArgumentParser(
114
- description='Interactively query Salesforce data with real-time autocompletion.'
115
- )
116
- parser.add_argument(
117
- '-a', '--sfdxAuthUrl', type=str, help='Salesforce auth url', default=os.environ.get('SFDX_AUTH_URL')
118
- )
119
- parser.add_argument(
120
- '--dry-run', action='store_true', help='Print the query without executing it', default=str(os.environ.get('SFQ_DRY_RUN')),
121
- )
122
- parser.add_argument(
123
- '--disable-fuzzy-completion', action='store_true', help='Disable fuzzy completion', default=str(os.environ.get('SFQ_DISABLE_FUZZY_COMPLETION')),
124
- )
125
- args = parser.parse_args()
126
-
127
- if not args.sfdxAuthUrl:
128
- raise ValueError('SFDX_AUTH_URL environment variable is not set nor provided as an argument')
129
-
130
- try:
131
- if args.dry_run.lower() not in ['true', '1']:
132
- args.dry_run = False
133
- except AttributeError:
134
- pass
135
-
136
- try:
137
- if args.disable_fuzzy_completion.lower() not in ['true', '1']:
138
- args.disable_fuzzy_completion = False
139
- except AttributeError:
140
- pass
141
-
142
-
143
- _interactive_shell(
144
- SFAuth(
145
- instance_url=f"https://{str(args.sfdxAuthUrl).split('@')[1]}",
146
- client_id=str(args.sfdxAuthUrl).split('//')[1].split('::')[0],
147
- refresh_token=str(args.sfdxAuthUrl).split('::')[1].split('@')[0],
148
- ),
149
- args.dry_run,
150
- args.disable_fuzzy_completion
151
- )
@@ -1,6 +0,0 @@
1
- sfq/__init__.py,sha256=UvDYd9Iu6Fh_S0TKujWXyClhCk5dsz2Yq1X7xFsPbT4,6597
2
- sfq/__main__.py,sha256=XDb-hRo7jkB3nbHJkQCxL0eUx2Sgl_C3Rr_peuOd6fs,5724
3
- sfq/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
- sfq-0.0.7.dist-info/METADATA,sha256=TZJnKAi1WddBKHXhDJrAxaLPhcbhI3jR0NBesnpcMnk,5491
5
- sfq-0.0.7.dist-info/WHEEL,sha256=KGYbc1zXlYddvwxnNty23BeaKzh7YuoSIvIMO4jEhvw,87
6
- sfq-0.0.7.dist-info/RECORD,,