sfq 0.0.3__py3-none-any.whl → 0.0.5__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,7 +5,7 @@ import os
|
|
5
5
|
import json
|
6
6
|
from urllib.parse import urlparse, quote
|
7
7
|
|
8
|
-
logging.basicConfig(level=logging.INFO)
|
8
|
+
logging.basicConfig(level=logging.INFO, format='[sfq:%(lineno)d - %(levelname)s] %(message)s')
|
9
9
|
|
10
10
|
class SFAuth:
|
11
11
|
def __init__(
|
@@ -108,7 +108,7 @@ class SFAuth:
|
|
108
108
|
"""Automatically refresh the token if it has expired or is missing."""
|
109
109
|
_token_expiration = self._is_token_expired()
|
110
110
|
if self.access_token and not _token_expiration:
|
111
|
-
return
|
111
|
+
return self.access_token
|
112
112
|
|
113
113
|
if not self.access_token:
|
114
114
|
logging.debug("No access token available. Requesting a new one.")
|
@@ -123,6 +123,7 @@ class SFAuth:
|
|
123
123
|
logging.debug("Access token refreshed successfully.")
|
124
124
|
else:
|
125
125
|
logging.error("Failed to refresh access token.")
|
126
|
+
return self.access_token
|
126
127
|
|
127
128
|
|
128
129
|
def _is_token_expired(self):
|
@@ -130,7 +131,7 @@ class SFAuth:
|
|
130
131
|
try:
|
131
132
|
return time.time() >= float(self.token_expiration_time)
|
132
133
|
except (TypeError, ValueError):
|
133
|
-
return
|
134
|
+
return True
|
134
135
|
|
135
136
|
def query(self, query, tooling=False):
|
136
137
|
"""Query Salesforce using SOQL or Tooling API, depending on the `tooling` parameter."""
|
sfq/__main__.py
ADDED
@@ -0,0 +1,153 @@
|
|
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
|
+
token = sf._refresh_token_if_needed()
|
51
|
+
headers = {'Authorization': f'Bearer {token}'}
|
52
|
+
print(f'token: {token}')
|
53
|
+
conn.request("GET", uri, headers=headers)
|
54
|
+
response = conn.getresponse()
|
55
|
+
|
56
|
+
if response.status != 200:
|
57
|
+
print(f'Error: {response.status} {response.reason}')
|
58
|
+
return []
|
59
|
+
|
60
|
+
data = json.loads(response.read())
|
61
|
+
return [sobject['name'] for sobject in data['sobjects']]
|
62
|
+
|
63
|
+
def _get_fields(sobject: str, sf: SFAuth):
|
64
|
+
"""Retrieve available fields for a given Salesforce object."""
|
65
|
+
host = sf.instance_url.split("://")[1].split("/")[0]
|
66
|
+
conn = http.client.HTTPSConnection(host)
|
67
|
+
uri = f"/services/data/{sf.api_version}/sobjects/{sobject}/describe/"
|
68
|
+
headers = {'Authorization': f'Bearer {sf._refresh_token_if_needed()}'}
|
69
|
+
conn.request("GET", uri, headers=headers)
|
70
|
+
response = conn.getresponse()
|
71
|
+
|
72
|
+
if response.status != 200:
|
73
|
+
print(f'Error: {response.status} {response.reason}')
|
74
|
+
raise ValueError(f'Unable to fetch fields for sObject "{sobject}": {response.status}, {response.reason}')
|
75
|
+
|
76
|
+
data = json.loads(response.read())
|
77
|
+
return [f['name'] for f in data['fields']]
|
78
|
+
|
79
|
+
available_objects = _get_objects(sf)
|
80
|
+
|
81
|
+
object_completer = DynamicSeparatorCompleter(available_objects)
|
82
|
+
while not sobject:
|
83
|
+
sobject = prompt('FROM ', completer=object_completer).strip()
|
84
|
+
|
85
|
+
|
86
|
+
available_fields = _get_fields(sobject, sf)
|
87
|
+
field_completer = DynamicSeparatorCompleter(available_fields, separators=[","])
|
88
|
+
while not fields:
|
89
|
+
fields = prompt('SELECT ', completer=field_completer).strip()
|
90
|
+
|
91
|
+
where_completer = DynamicSeparatorCompleter(available_fields, separators=[" AND ", " OR "])
|
92
|
+
where = prompt("WHERE ", completer=where_completer).strip()
|
93
|
+
where_clause = f"WHERE {where}" if where else ""
|
94
|
+
|
95
|
+
limit = prompt("LIMIT ", default="200").strip()
|
96
|
+
limit_clause = f"LIMIT {limit}" if limit else ""
|
97
|
+
|
98
|
+
query = f"SELECT {fields} FROM {sobject} {where_clause} {limit_clause}".replace(' ', ' ')
|
99
|
+
|
100
|
+
if dry_run:
|
101
|
+
print('\nDry-run, skipping execution...')
|
102
|
+
print(f'\nQuery: {query}\n')
|
103
|
+
return query
|
104
|
+
|
105
|
+
print('\nExecuting query...\n')
|
106
|
+
data = sf.query(query)
|
107
|
+
print(json.dumps(data, indent=4))
|
108
|
+
print(f'\nQuery: {query}\n')
|
109
|
+
return data
|
110
|
+
|
111
|
+
if __name__ == "__main__":
|
112
|
+
import argparse
|
113
|
+
import os
|
114
|
+
|
115
|
+
parser = argparse.ArgumentParser(
|
116
|
+
description='Interactively query Salesforce data with real-time autocompletion.'
|
117
|
+
)
|
118
|
+
parser.add_argument(
|
119
|
+
'-a', '--sfdxAuthUrl', type=str, help='Salesforce auth url', default=os.environ.get('SFDX_AUTH_URL')
|
120
|
+
)
|
121
|
+
parser.add_argument(
|
122
|
+
'--dry-run', action='store_true', help='Print the query without executing it', default=str(os.environ.get('SFQ_DRY_RUN')),
|
123
|
+
)
|
124
|
+
parser.add_argument(
|
125
|
+
'--disable-fuzzy-completion', action='store_true', help='Disable fuzzy completion', default=str(os.environ.get('SFQ_DISABLE_FUZZY_COMPLETION')),
|
126
|
+
)
|
127
|
+
args = parser.parse_args()
|
128
|
+
|
129
|
+
if not args.sfdxAuthUrl:
|
130
|
+
raise ValueError('SFDX_AUTH_URL environment variable is not set nor provided as an argument')
|
131
|
+
|
132
|
+
try:
|
133
|
+
if args.dry_run.lower() not in ['true', '1']:
|
134
|
+
args.dry_run = False
|
135
|
+
except AttributeError:
|
136
|
+
pass
|
137
|
+
|
138
|
+
try:
|
139
|
+
if args.disable_fuzzy_completion.lower() not in ['true', '1']:
|
140
|
+
args.disable_fuzzy_completion = False
|
141
|
+
except AttributeError:
|
142
|
+
pass
|
143
|
+
|
144
|
+
|
145
|
+
_interactive_shell(
|
146
|
+
SFAuth(
|
147
|
+
instance_url=f"https://{str(args.sfdxAuthUrl).split('@')[1]}",
|
148
|
+
client_id=str(args.sfdxAuthUrl).split('//')[1].split('::')[0],
|
149
|
+
refresh_token=str(args.sfdxAuthUrl).split('::')[1].split('@')[0],
|
150
|
+
),
|
151
|
+
args.dry_run,
|
152
|
+
args.disable_fuzzy_completion
|
153
|
+
)
|
@@ -1,17 +1,20 @@
|
|
1
|
-
Metadata-Version: 2.
|
1
|
+
Metadata-Version: 2.1
|
2
2
|
Name: sfq
|
3
|
-
Version: 0.0.
|
3
|
+
Version: 0.0.5
|
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
|
9
11
|
Classifier: Programming Language :: Python :: 3.9
|
10
12
|
Classifier: Programming Language :: Python :: 3.10
|
11
13
|
Classifier: Programming Language :: Python :: 3.12
|
12
14
|
Classifier: Programming Language :: Python :: 3.13
|
13
15
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
14
|
-
Requires-Python: >=3.
|
16
|
+
Requires-Python: >=3.7
|
17
|
+
Requires-Dist: prompt-toolkit>=3.0.3
|
15
18
|
Description-Content-Type: text/markdown
|
16
19
|
|
17
20
|
# sfq (Salesforce Query)
|
@@ -36,7 +39,25 @@ pip install sfq
|
|
36
39
|
|
37
40
|
## Usage
|
38
41
|
|
39
|
-
### Querying
|
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
|
+
### Library Querying
|
40
61
|
|
41
62
|
```python
|
42
63
|
from sfq import SFAuth
|
@@ -55,7 +76,7 @@ print(sf.query("SELECT Id FROM Account LIMIT 5"))
|
|
55
76
|
print(sf.query("SELECT Id, FullName, Metadata FROM SandboxSettings LIMIT 5", tooling=True))
|
56
77
|
```
|
57
78
|
|
58
|
-
### Querying
|
79
|
+
### Bash Querying
|
59
80
|
|
60
81
|
You can easily incorporate this into ad-hoc bash scripts or commands:
|
61
82
|
|
@@ -128,4 +149,4 @@ To use the `sfq` library, you'll need a **client ID** and **refresh token**. The
|
|
128
149
|
## Notes
|
129
150
|
|
130
151
|
- **Authentication**: Make sure your refresh token is kept secure, as it grants access to your Salesforce instance.
|
131
|
-
- **Tooling API**: You can set the `tooling=True` argument in the `query` method to access the Salesforce Tooling API for more advanced metadata queries.
|
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,6 @@
|
|
1
|
+
sfq/__init__.py,sha256=sIZr8eUzWV9q2PEenbCnu4mRBmv07FC7zjfpcjux_1g,6466
|
2
|
+
sfq/__main__.py,sha256=CqwwG2l6ZphmiOSeqQeB7W73hTlGKJRDYtlKLkM7Bps,5779
|
3
|
+
sfq/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
4
|
+
sfq-0.0.5.dist-info/METADATA,sha256=1f9a_TSpoLzcyMbqRrxwVyCqE5vj_WCivEg6uxNiNxQ,5491
|
5
|
+
sfq-0.0.5.dist-info/WHEEL,sha256=KGYbc1zXlYddvwxnNty23BeaKzh7YuoSIvIMO4jEhvw,87
|
6
|
+
sfq-0.0.5.dist-info/RECORD,,
|
sfq-0.0.3.dist-info/RECORD
DELETED
@@ -1,5 +0,0 @@
|
|
1
|
-
sfq/__init__.py,sha256=vcAXd4v6uyjzAqOril31LG2WG2n3l6OHxPiYQA63uD4,6361
|
2
|
-
sfq/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
3
|
-
sfq-0.0.3.dist-info/METADATA,sha256=y5Ir5XtomFESBbaktjuWzsA2d0Q40qlMIBXx1igISU4,4685
|
4
|
-
sfq-0.0.3.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
5
|
-
sfq-0.0.3.dist-info/RECORD,,
|