sfq 0.0.11__tar.gz → 0.0.13__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.13/PKG-INFO +179 -0
- sfq-0.0.13/README.md +163 -0
- {sfq-0.0.11 → sfq-0.0.13}/pyproject.toml +1 -1
- {sfq-0.0.11 → sfq-0.0.13}/src/sfq/__init__.py +280 -28
- {sfq-0.0.11 → sfq-0.0.13}/uv.lock +1 -1
- sfq-0.0.11/PKG-INFO +0 -133
- sfq-0.0.11/README.md +0 -117
- {sfq-0.0.11 → sfq-0.0.13}/.github/workflows/publish.yml +0 -0
- {sfq-0.0.11 → sfq-0.0.13}/.gitignore +0 -0
- {sfq-0.0.11 → sfq-0.0.13}/.python-version +0 -0
- {sfq-0.0.11 → sfq-0.0.13}/src/sfq/py.typed +0 -0
sfq-0.0.13/PKG-INFO
ADDED
@@ -0,0 +1,179 @@
|
|
1
|
+
Metadata-Version: 2.4
|
2
|
+
Name: sfq
|
3
|
+
Version: 0.0.13
|
4
|
+
Summary: Python wrapper for the Salesforce's Query API.
|
5
|
+
Author-email: David Moruzzi <sfq.pypi@dmoruzi.com>
|
6
|
+
Keywords: salesforce,salesforce query
|
7
|
+
Classifier: Development Status :: 3 - Alpha
|
8
|
+
Classifier: Intended Audience :: Developers
|
9
|
+
Classifier: Programming Language :: Python :: 3.9
|
10
|
+
Classifier: Programming Language :: Python :: 3.10
|
11
|
+
Classifier: Programming Language :: Python :: 3.12
|
12
|
+
Classifier: Programming Language :: Python :: 3.13
|
13
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
14
|
+
Requires-Python: >=3.9
|
15
|
+
Description-Content-Type: text/markdown
|
16
|
+
|
17
|
+
# sfq (Salesforce Query)
|
18
|
+
|
19
|
+
sfq is a lightweight Python wrapper library designed to simplify querying Salesforce, reducing repetitive code for accessing Salesforce data.
|
20
|
+
|
21
|
+
For more varied workflows, consider using an alternative like [Simple Salesforce](https://simple-salesforce.readthedocs.io/en/stable/). This library was even referenced on the [Salesforce Developers Blog](https://developer.salesforce.com/blogs/2021/09/how-to-automate-data-extraction-from-salesforce-using-python).
|
22
|
+
|
23
|
+
## Features
|
24
|
+
|
25
|
+
- Simplified query execution for Salesforce instances.
|
26
|
+
- Integration with Salesforce authentication via refresh tokens.
|
27
|
+
- Option to interact with Salesforce Tooling API for more advanced queries.
|
28
|
+
|
29
|
+
## Installation
|
30
|
+
|
31
|
+
You can install the `sfq` library using `pip`:
|
32
|
+
|
33
|
+
```bash
|
34
|
+
pip install sfq
|
35
|
+
```
|
36
|
+
|
37
|
+
## Usage
|
38
|
+
|
39
|
+
### Library Querying
|
40
|
+
|
41
|
+
```python
|
42
|
+
from sfq import SFAuth
|
43
|
+
|
44
|
+
# Initialize the SFAuth class with authentication details
|
45
|
+
sf = SFAuth(
|
46
|
+
instance_url="https://example-dev-ed.trailblaze.my.salesforce.com",
|
47
|
+
client_id="your-client-id-here",
|
48
|
+
client_secret="your-client-secret-here",
|
49
|
+
refresh_token="your-refresh-token-here"
|
50
|
+
)
|
51
|
+
|
52
|
+
# Execute a query to fetch account records
|
53
|
+
print(sf.query("SELECT Id FROM Account LIMIT 5"))
|
54
|
+
|
55
|
+
# Execute a query to fetch Tooling API data
|
56
|
+
print(sf.tooling_query("SELECT Id, FullName, Metadata FROM SandboxSettings LIMIT 5"))
|
57
|
+
```
|
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
|
+
### Composite Batch Queries
|
72
|
+
|
73
|
+
```python
|
74
|
+
multiple_queries = {
|
75
|
+
"Recent Users": """
|
76
|
+
SELECT Id, Name,CreatedDate
|
77
|
+
FROM User
|
78
|
+
ORDER BY CreatedDate DESC
|
79
|
+
LIMIT 10""",
|
80
|
+
"Recent Accounts": "SELECT Id, Name, CreatedDate FROM Account ORDER BY CreatedDate DESC LIMIT 10",
|
81
|
+
"Frozen Users": "SELECT Id, UserId FROM UserLogin WHERE IsFrozen = true", # If exceeds 2000 records, will paginate
|
82
|
+
}
|
83
|
+
|
84
|
+
batched_response = sf.cquery(multiple_queries)
|
85
|
+
|
86
|
+
for subrequest_identifer, subrequest_response in batched_response.items():
|
87
|
+
print(f'"{subrequest_identifer}" returned {subrequest_response["totalSize"]} records')
|
88
|
+
>>> "Recent Users" returned 10 records
|
89
|
+
>>> "Recent Accounts" returned 10 records
|
90
|
+
>>> "Frozen Users" returned 4082 records
|
91
|
+
```
|
92
|
+
|
93
|
+
### Static Resources
|
94
|
+
|
95
|
+
```python
|
96
|
+
page = sf.read_static_resource_id('081aj000009jUMXAA2')
|
97
|
+
print(f'Initial resource: {page}')
|
98
|
+
>>> Initial resource: <h1>It works!</h1>
|
99
|
+
sf.update_static_resource_name('HelloWorld', '<h1>Hello World</h1>')
|
100
|
+
page = sf.read_static_resource_name('HelloWorld')
|
101
|
+
print(f'Updated resource: {page}')
|
102
|
+
>>> Updated resource: <h1>Hello World</h1>
|
103
|
+
sf.update_static_resource_id('081aj000009jUMXAA2', '<h1>It works!</h1>')
|
104
|
+
```
|
105
|
+
|
106
|
+
## How to Obtain Salesforce Tokens
|
107
|
+
|
108
|
+
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:
|
109
|
+
|
110
|
+
### Steps to Get Tokens
|
111
|
+
|
112
|
+
1. **Install the Salesforce CLI**:
|
113
|
+
Follow the instructions on the [Salesforce CLI installation page](https://developer.salesforce.com/tools/salesforcecli).
|
114
|
+
|
115
|
+
2. **Authenticate with Salesforce**:
|
116
|
+
Login to your Salesforce org using the following command:
|
117
|
+
|
118
|
+
```bash
|
119
|
+
sf org login web --alias int --instance-url https://corpa--int.sandbox.my.salesforce.com
|
120
|
+
```
|
121
|
+
|
122
|
+
3. **Display Org Details**:
|
123
|
+
To get the client ID, client secret, refresh token, and instance URL, run:
|
124
|
+
|
125
|
+
```bash
|
126
|
+
sf org display --target-org int --verbose --json
|
127
|
+
```
|
128
|
+
|
129
|
+
The output will look like this:
|
130
|
+
|
131
|
+
```json
|
132
|
+
{
|
133
|
+
"status": 0,
|
134
|
+
"result": {
|
135
|
+
"id": "00Daa0000000000000",
|
136
|
+
"apiVersion": "63.0",
|
137
|
+
"accessToken": "00Daa0000000000000!evaU3fYZEWGUrqI5rMtaS8KYbHfeqA7YWzMgKToOB43Jk0kj7LtiWCbJaj4owPFQ7CqpXPAGX1RDCHblfW9t8cNOCNRFng3o",
|
138
|
+
"instanceUrl": "https://example-dev-ed.trailblaze.my.salesforce.com",
|
139
|
+
"username": "user@example.com",
|
140
|
+
"clientId": "PlatformCLI",
|
141
|
+
"connectedStatus": "Connected",
|
142
|
+
"sfdxAuthUrl": "force://PlatformCLI::nwAeSuiRqvRHrkbMmCKvLHasS0vRbh3Cf2RF41WZzmjtThnCwOuDvn9HObcUjKuTExJPqPegIwnLB5aH6GNWYhU@example-dev-ed.trailblaze.my.salesforce.com",
|
143
|
+
"alias": "int"
|
144
|
+
}
|
145
|
+
}
|
146
|
+
```
|
147
|
+
|
148
|
+
4. **Extract and Use the Tokens**:
|
149
|
+
The `sfdxAuthUrl` is structured as:
|
150
|
+
|
151
|
+
```
|
152
|
+
force://<client_id>:<client_secret>:<refresh_token>@<instance_url>
|
153
|
+
```
|
154
|
+
|
155
|
+
This means with the above output sample, you would use the following information:
|
156
|
+
|
157
|
+
```python
|
158
|
+
# This is for illustrative purposes; use environment variables instead
|
159
|
+
client_id = "PlatformCLI"
|
160
|
+
client_secret = ""
|
161
|
+
refresh_token = "nwAeSuiRqvRHrkbMmCKvLHasS0vRbh3Cf2RF41WZzmjtThnCwOuDvn9HObcUjKuTExJPqPegIwnLB5aH6GNWYhU"
|
162
|
+
instance_url = "https://example-dev-ed.trailblaze.my.salesforce.com"
|
163
|
+
|
164
|
+
from sfq import SFAuth
|
165
|
+
sf = SFAuth(
|
166
|
+
instance_url=instance_url,
|
167
|
+
client_id=client_id,
|
168
|
+
client_secret=client_secret,
|
169
|
+
refresh_token=refresh_token,
|
170
|
+
)
|
171
|
+
|
172
|
+
```
|
173
|
+
|
174
|
+
## Important Considerations
|
175
|
+
|
176
|
+
- **Security**: Safeguard your client_id, client_secret, and refresh_token diligently, as they provide access to your Salesforce environment. Avoid sharing or exposing them in unsecured locations.
|
177
|
+
- **Efficient Data Retrieval**: The `query` and `cquery` 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.
|
178
|
+
- **Advanced Tooling Queries**: Utilize the `tooling_query` function to access the Salesforce Tooling API. This option is designed for performing complex operations, enhancing your data management capabilities.
|
179
|
+
|
sfq-0.0.13/README.md
ADDED
@@ -0,0 +1,163 @@
|
|
1
|
+
# sfq (Salesforce Query)
|
2
|
+
|
3
|
+
sfq is a lightweight Python wrapper library designed to simplify querying Salesforce, reducing repetitive code for accessing Salesforce data.
|
4
|
+
|
5
|
+
For more varied workflows, consider using an alternative like [Simple Salesforce](https://simple-salesforce.readthedocs.io/en/stable/). This library was even referenced on the [Salesforce Developers Blog](https://developer.salesforce.com/blogs/2021/09/how-to-automate-data-extraction-from-salesforce-using-python).
|
6
|
+
|
7
|
+
## Features
|
8
|
+
|
9
|
+
- Simplified query execution for Salesforce instances.
|
10
|
+
- Integration with Salesforce authentication via refresh tokens.
|
11
|
+
- Option to interact with Salesforce Tooling API for more advanced queries.
|
12
|
+
|
13
|
+
## Installation
|
14
|
+
|
15
|
+
You can install the `sfq` library using `pip`:
|
16
|
+
|
17
|
+
```bash
|
18
|
+
pip install sfq
|
19
|
+
```
|
20
|
+
|
21
|
+
## Usage
|
22
|
+
|
23
|
+
### Library Querying
|
24
|
+
|
25
|
+
```python
|
26
|
+
from sfq import SFAuth
|
27
|
+
|
28
|
+
# Initialize the SFAuth class with authentication details
|
29
|
+
sf = SFAuth(
|
30
|
+
instance_url="https://example-dev-ed.trailblaze.my.salesforce.com",
|
31
|
+
client_id="your-client-id-here",
|
32
|
+
client_secret="your-client-secret-here",
|
33
|
+
refresh_token="your-refresh-token-here"
|
34
|
+
)
|
35
|
+
|
36
|
+
# Execute a query to fetch account records
|
37
|
+
print(sf.query("SELECT Id FROM Account LIMIT 5"))
|
38
|
+
|
39
|
+
# Execute a query to fetch Tooling API data
|
40
|
+
print(sf.tooling_query("SELECT Id, FullName, Metadata FROM SandboxSettings LIMIT 5"))
|
41
|
+
```
|
42
|
+
|
43
|
+
### sObject Key Prefixes
|
44
|
+
|
45
|
+
```python
|
46
|
+
# Key prefix via IDs
|
47
|
+
print(sf.get_sobject_prefixes())
|
48
|
+
>>> {'0Pp': 'AIApplication', '6S9': 'AIApplicationConfig', '9qd': 'AIInsightAction', '9bq': 'AIInsightFeedback', '0T2': 'AIInsightReason', '9qc': 'AIInsightValue', ...}
|
49
|
+
|
50
|
+
# Key prefix via names
|
51
|
+
print(sf.get_sobject_prefixes(key_type="name"))
|
52
|
+
>>> {'AIApplication': '0Pp', 'AIApplicationConfig': '6S9', 'AIInsightAction': '9qd', 'AIInsightFeedback': '9bq', 'AIInsightReason': '0T2', 'AIInsightValue': '9qc', ...}
|
53
|
+
```
|
54
|
+
|
55
|
+
### Composite Batch Queries
|
56
|
+
|
57
|
+
```python
|
58
|
+
multiple_queries = {
|
59
|
+
"Recent Users": """
|
60
|
+
SELECT Id, Name,CreatedDate
|
61
|
+
FROM User
|
62
|
+
ORDER BY CreatedDate DESC
|
63
|
+
LIMIT 10""",
|
64
|
+
"Recent Accounts": "SELECT Id, Name, CreatedDate FROM Account ORDER BY CreatedDate DESC LIMIT 10",
|
65
|
+
"Frozen Users": "SELECT Id, UserId FROM UserLogin WHERE IsFrozen = true", # If exceeds 2000 records, will paginate
|
66
|
+
}
|
67
|
+
|
68
|
+
batched_response = sf.cquery(multiple_queries)
|
69
|
+
|
70
|
+
for subrequest_identifer, subrequest_response in batched_response.items():
|
71
|
+
print(f'"{subrequest_identifer}" returned {subrequest_response["totalSize"]} records')
|
72
|
+
>>> "Recent Users" returned 10 records
|
73
|
+
>>> "Recent Accounts" returned 10 records
|
74
|
+
>>> "Frozen Users" returned 4082 records
|
75
|
+
```
|
76
|
+
|
77
|
+
### Static Resources
|
78
|
+
|
79
|
+
```python
|
80
|
+
page = sf.read_static_resource_id('081aj000009jUMXAA2')
|
81
|
+
print(f'Initial resource: {page}')
|
82
|
+
>>> Initial resource: <h1>It works!</h1>
|
83
|
+
sf.update_static_resource_name('HelloWorld', '<h1>Hello World</h1>')
|
84
|
+
page = sf.read_static_resource_name('HelloWorld')
|
85
|
+
print(f'Updated resource: {page}')
|
86
|
+
>>> Updated resource: <h1>Hello World</h1>
|
87
|
+
sf.update_static_resource_id('081aj000009jUMXAA2', '<h1>It works!</h1>')
|
88
|
+
```
|
89
|
+
|
90
|
+
## How to Obtain Salesforce Tokens
|
91
|
+
|
92
|
+
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:
|
93
|
+
|
94
|
+
### Steps to Get Tokens
|
95
|
+
|
96
|
+
1. **Install the Salesforce CLI**:
|
97
|
+
Follow the instructions on the [Salesforce CLI installation page](https://developer.salesforce.com/tools/salesforcecli).
|
98
|
+
|
99
|
+
2. **Authenticate with Salesforce**:
|
100
|
+
Login to your Salesforce org using the following command:
|
101
|
+
|
102
|
+
```bash
|
103
|
+
sf org login web --alias int --instance-url https://corpa--int.sandbox.my.salesforce.com
|
104
|
+
```
|
105
|
+
|
106
|
+
3. **Display Org Details**:
|
107
|
+
To get the client ID, client secret, refresh token, and instance URL, run:
|
108
|
+
|
109
|
+
```bash
|
110
|
+
sf org display --target-org int --verbose --json
|
111
|
+
```
|
112
|
+
|
113
|
+
The output will look like this:
|
114
|
+
|
115
|
+
```json
|
116
|
+
{
|
117
|
+
"status": 0,
|
118
|
+
"result": {
|
119
|
+
"id": "00Daa0000000000000",
|
120
|
+
"apiVersion": "63.0",
|
121
|
+
"accessToken": "00Daa0000000000000!evaU3fYZEWGUrqI5rMtaS8KYbHfeqA7YWzMgKToOB43Jk0kj7LtiWCbJaj4owPFQ7CqpXPAGX1RDCHblfW9t8cNOCNRFng3o",
|
122
|
+
"instanceUrl": "https://example-dev-ed.trailblaze.my.salesforce.com",
|
123
|
+
"username": "user@example.com",
|
124
|
+
"clientId": "PlatformCLI",
|
125
|
+
"connectedStatus": "Connected",
|
126
|
+
"sfdxAuthUrl": "force://PlatformCLI::nwAeSuiRqvRHrkbMmCKvLHasS0vRbh3Cf2RF41WZzmjtThnCwOuDvn9HObcUjKuTExJPqPegIwnLB5aH6GNWYhU@example-dev-ed.trailblaze.my.salesforce.com",
|
127
|
+
"alias": "int"
|
128
|
+
}
|
129
|
+
}
|
130
|
+
```
|
131
|
+
|
132
|
+
4. **Extract and Use the Tokens**:
|
133
|
+
The `sfdxAuthUrl` is structured as:
|
134
|
+
|
135
|
+
```
|
136
|
+
force://<client_id>:<client_secret>:<refresh_token>@<instance_url>
|
137
|
+
```
|
138
|
+
|
139
|
+
This means with the above output sample, you would use the following information:
|
140
|
+
|
141
|
+
```python
|
142
|
+
# This is for illustrative purposes; use environment variables instead
|
143
|
+
client_id = "PlatformCLI"
|
144
|
+
client_secret = ""
|
145
|
+
refresh_token = "nwAeSuiRqvRHrkbMmCKvLHasS0vRbh3Cf2RF41WZzmjtThnCwOuDvn9HObcUjKuTExJPqPegIwnLB5aH6GNWYhU"
|
146
|
+
instance_url = "https://example-dev-ed.trailblaze.my.salesforce.com"
|
147
|
+
|
148
|
+
from sfq import SFAuth
|
149
|
+
sf = SFAuth(
|
150
|
+
instance_url=instance_url,
|
151
|
+
client_id=client_id,
|
152
|
+
client_secret=client_secret,
|
153
|
+
refresh_token=refresh_token,
|
154
|
+
)
|
155
|
+
|
156
|
+
```
|
157
|
+
|
158
|
+
## Important Considerations
|
159
|
+
|
160
|
+
- **Security**: Safeguard your client_id, client_secret, and refresh_token diligently, as they provide access to your Salesforce environment. Avoid sharing or exposing them in unsecured locations.
|
161
|
+
- **Efficient Data Retrieval**: The `query` and `cquery` 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.
|
162
|
+
- **Advanced Tooling Queries**: Utilize the `tooling_query` function to access the Salesforce Tooling API. This option is designed for performing complex operations, enhancing your data management capabilities.
|
163
|
+
|
@@ -5,16 +5,20 @@ 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
|
-
from typing import Any, Dict, Optional
|
11
|
+
from typing import Any, Dict, Literal, Optional
|
10
12
|
from urllib.parse import quote, urlparse
|
11
13
|
|
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
|
|
@@ -77,13 +81,14 @@ class SFAuth:
|
|
77
81
|
self,
|
78
82
|
instance_url: str,
|
79
83
|
client_id: str,
|
80
|
-
refresh_token: str,
|
84
|
+
refresh_token: str, # client_secret & refresh_token will swap positions 2025-AUG-1
|
85
|
+
client_secret: str = "_deprecation_warning", # mandatory after 2025-AUG-1
|
81
86
|
api_version: str = "v63.0",
|
82
87
|
token_endpoint: str = "/services/oauth2/token",
|
83
88
|
access_token: Optional[str] = None,
|
84
89
|
token_expiration_time: Optional[float] = None,
|
85
90
|
token_lifetime: int = 15 * 60,
|
86
|
-
user_agent: str = "sfq/0.0.
|
91
|
+
user_agent: str = "sfq/0.0.13",
|
87
92
|
proxy: str = "auto",
|
88
93
|
) -> None:
|
89
94
|
"""
|
@@ -92,16 +97,18 @@ class SFAuth:
|
|
92
97
|
:param instance_url: The Salesforce instance URL.
|
93
98
|
:param client_id: The client ID for OAuth.
|
94
99
|
:param refresh_token: The refresh token for OAuth.
|
100
|
+
:param client_secret: The client secret for OAuth (default is "_deprecation_warning").
|
95
101
|
:param api_version: The Salesforce API version (default is "v63.0").
|
96
102
|
:param token_endpoint: The token endpoint (default is "/services/oauth2/token").
|
97
103
|
:param access_token: The access token for the current session (default is None).
|
98
104
|
:param token_expiration_time: The expiration time of the access token (default is None).
|
99
105
|
: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.
|
106
|
+
:param user_agent: Custom User-Agent string (default is "sfq/0.0.13").
|
101
107
|
:param proxy: The proxy configuration, "auto" to use environment (default is "auto").
|
102
108
|
"""
|
103
|
-
self.instance_url = instance_url
|
109
|
+
self.instance_url = self._format_instance_url(instance_url)
|
104
110
|
self.client_id = client_id
|
111
|
+
self.client_secret = client_secret
|
105
112
|
self.refresh_token = refresh_token
|
106
113
|
self.api_version = api_version
|
107
114
|
self.token_endpoint = token_endpoint
|
@@ -112,6 +119,26 @@ class SFAuth:
|
|
112
119
|
self._auto_configure_proxy(proxy)
|
113
120
|
self._high_api_usage_threshold = 80
|
114
121
|
|
122
|
+
if self.client_secret == "_deprecation_warning":
|
123
|
+
warnings.warn(
|
124
|
+
"The 'client_secret' parameter will be mandatory and positional arguments will change after 1 August 2025. "
|
125
|
+
"Please ensure explicit argument assignment and 'client_secret' inclusion when initializing the SFAuth object.",
|
126
|
+
DeprecationWarning,
|
127
|
+
stacklevel=2,
|
128
|
+
)
|
129
|
+
|
130
|
+
logger.debug(
|
131
|
+
"Will be SFAuth(instance_url, client_id, client_secret, refresh_token) starting 1 August 2025... but please just use named arguments.."
|
132
|
+
)
|
133
|
+
|
134
|
+
def _format_instance_url(self, instance_url) -> str:
|
135
|
+
# check if it begins with https://
|
136
|
+
if instance_url.startswith("https://"):
|
137
|
+
return instance_url
|
138
|
+
if instance_url.startswith("http://"):
|
139
|
+
return instance_url.replace("http://", "https://")
|
140
|
+
return f"https://{instance_url}"
|
141
|
+
|
115
142
|
def _auto_configure_proxy(self, proxy: str) -> None:
|
116
143
|
"""
|
117
144
|
Automatically configure the proxy based on the environment or provided value.
|
@@ -124,16 +151,36 @@ class SFAuth:
|
|
124
151
|
self.proxy = proxy
|
125
152
|
logger.debug("Using configured proxy: %s", self.proxy)
|
126
153
|
|
127
|
-
def _prepare_payload(self) -> Dict[str, str]:
|
154
|
+
def _prepare_payload(self) -> Dict[str, Optional[str]]:
|
128
155
|
"""
|
129
156
|
Prepare the payload for the token request.
|
157
|
+
|
158
|
+
This method constructs a dictionary containing the necessary parameters
|
159
|
+
for a token request using the refresh token grant type. It includes
|
160
|
+
the client ID, client secret, and refresh token if they are available.
|
161
|
+
|
162
|
+
Returns:
|
163
|
+
Dict[str, Optional[str]]: A dictionary containing the payload for the token request.
|
130
164
|
"""
|
131
|
-
|
165
|
+
payload = {
|
132
166
|
"grant_type": "refresh_token",
|
133
167
|
"client_id": self.client_id,
|
168
|
+
"client_secret": self.client_secret,
|
134
169
|
"refresh_token": self.refresh_token,
|
135
170
|
}
|
136
171
|
|
172
|
+
if self.client_secret == "_deprecation_warning":
|
173
|
+
logger.warning(
|
174
|
+
"The SFQ library is making a breaking change (2025-AUG-1) to require the 'client_secret' parameter to be assigned when initializing the SFAuth object. "
|
175
|
+
"In addition, positional arguments will change. Please ensure explicit argument assignment and 'client_secret' inclusion when initializing the SFAuth object to avoid impact."
|
176
|
+
)
|
177
|
+
payload.pop("client_secret")
|
178
|
+
|
179
|
+
if not self.client_secret:
|
180
|
+
payload.pop("client_secret")
|
181
|
+
|
182
|
+
return payload
|
183
|
+
|
137
184
|
def _create_connection(self, netloc: str) -> http.client.HTTPConnection:
|
138
185
|
"""
|
139
186
|
Create a connection using HTTP or HTTPS, with optional proxy support.
|
@@ -210,27 +257,24 @@ class SFAuth:
|
|
210
257
|
headers_list = [(k, v) for k, v in headers if not v.startswith("BrowserId=")]
|
211
258
|
logger.trace("Response headers: %s", headers_list)
|
212
259
|
for key, value in headers_list:
|
213
|
-
if key
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
260
|
+
if key == "Sforce-Limit-Info":
|
261
|
+
current_api_calls = int(value.split("=")[1].split("/")[0])
|
262
|
+
maximum_api_calls = int(value.split("=")[1].split("/")[1])
|
263
|
+
usage_percentage = round(current_api_calls / maximum_api_calls * 100, 2)
|
264
|
+
if usage_percentage > self._high_api_usage_threshold:
|
265
|
+
logger.warning(
|
266
|
+
"High API usage: %s/%s (%s%%)",
|
267
|
+
current_api_calls,
|
268
|
+
maximum_api_calls,
|
269
|
+
usage_percentage,
|
270
|
+
)
|
271
|
+
else:
|
272
|
+
logger.debug(
|
273
|
+
"API usage: %s/%s (%s%%)",
|
274
|
+
current_api_calls,
|
275
|
+
maximum_api_calls,
|
276
|
+
usage_percentage,
|
219
277
|
)
|
220
|
-
if usage_percentage > self._high_api_usage_threshold:
|
221
|
-
logger.warning(
|
222
|
-
"High API usage: %s/%s (%s%%)",
|
223
|
-
current_api_calls,
|
224
|
-
maximum_api_calls,
|
225
|
-
usage_percentage,
|
226
|
-
)
|
227
|
-
else:
|
228
|
-
logger.debug(
|
229
|
-
"API usage: %s/%s (%s%%)",
|
230
|
-
current_api_calls,
|
231
|
-
maximum_api_calls,
|
232
|
-
usage_percentage,
|
233
|
-
)
|
234
278
|
|
235
279
|
def _refresh_token_if_needed(self) -> Optional[str]:
|
236
280
|
"""
|
@@ -294,6 +338,7 @@ class SFAuth:
|
|
294
338
|
_safe_resource_name = quote(resource_name, safe="")
|
295
339
|
query = f"SELECT Id FROM StaticResource WHERE Name = '{_safe_resource_name}'"
|
296
340
|
if namespace:
|
341
|
+
namespace = quote(namespace, safe="")
|
297
342
|
query += f" AND NamespacePrefix = '{namespace}'"
|
298
343
|
query += " LIMIT 1"
|
299
344
|
_static_resource_id_response = self.query(query)
|
@@ -378,6 +423,7 @@ class SFAuth:
|
|
378
423
|
safe_resource_name = quote(resource_name, safe="")
|
379
424
|
query = f"SELECT Id FROM StaticResource WHERE Name = '{safe_resource_name}'"
|
380
425
|
if namespace:
|
426
|
+
namespace = quote(namespace, safe="")
|
381
427
|
query += f" AND NamespacePrefix = '{namespace}'"
|
382
428
|
query += " LIMIT 1"
|
383
429
|
|
@@ -603,13 +649,219 @@ class SFAuth:
|
|
603
649
|
"""
|
604
650
|
return self.query(query, tooling=True)
|
605
651
|
|
652
|
+
def get_sobject_prefixes(
|
653
|
+
self, key_type: Literal["id", "name"] = "id"
|
654
|
+
) -> Optional[Dict[str, str]]:
|
655
|
+
"""
|
656
|
+
Fetch all key prefixes from the Salesforce instance and map them to sObject names or vice versa.
|
657
|
+
|
658
|
+
:param key_type: The type of key to return. Either 'id' (prefix) or 'name' (sObject).
|
659
|
+
:return: A dictionary mapping key prefixes to sObject names or None on failure.
|
660
|
+
"""
|
661
|
+
valid_key_types = {"id", "name"}
|
662
|
+
if key_type not in valid_key_types:
|
663
|
+
logger.error(
|
664
|
+
"Invalid key type: %s, must be one of: %s",
|
665
|
+
key_type,
|
666
|
+
", ".join(valid_key_types),
|
667
|
+
)
|
668
|
+
return None
|
669
|
+
|
670
|
+
self._refresh_token_if_needed()
|
671
|
+
|
672
|
+
if not self.access_token:
|
673
|
+
logger.error("No access token available for key prefixes.")
|
674
|
+
return None
|
675
|
+
|
676
|
+
endpoint = f"/services/data/{self.api_version}/sobjects/"
|
677
|
+
headers = {
|
678
|
+
"Authorization": f"Bearer {self.access_token}",
|
679
|
+
"User-Agent": self.user_agent,
|
680
|
+
"Accept": "application/json",
|
681
|
+
}
|
682
|
+
|
683
|
+
parsed_url = urlparse(self.instance_url)
|
684
|
+
conn = self._create_connection(parsed_url.netloc)
|
685
|
+
prefixes = {}
|
686
|
+
|
687
|
+
try:
|
688
|
+
logger.trace("Request endpoint: %s", endpoint)
|
689
|
+
logger.trace("Request headers: %s", headers)
|
690
|
+
conn.request("GET", endpoint, headers=headers)
|
691
|
+
response = conn.getresponse()
|
692
|
+
data = response.read().decode("utf-8")
|
693
|
+
self._http_resp_header_logic(response)
|
694
|
+
|
695
|
+
if response.status == 200:
|
696
|
+
logger.debug("Key prefixes API request successful.")
|
697
|
+
logger.trace("Response body: %s", data)
|
698
|
+
for sobject in json.loads(data)["sobjects"]:
|
699
|
+
key_prefix = sobject.get("keyPrefix")
|
700
|
+
name = sobject.get("name")
|
701
|
+
if not key_prefix or not name:
|
702
|
+
continue
|
703
|
+
|
704
|
+
if key_type == "id":
|
705
|
+
prefixes[key_prefix] = name
|
706
|
+
elif key_type == "name":
|
707
|
+
prefixes[name] = key_prefix
|
708
|
+
|
709
|
+
logger.debug("Key prefixes: %s", prefixes)
|
710
|
+
return prefixes
|
711
|
+
|
712
|
+
logger.error(
|
713
|
+
"Key prefixes API request failed: %s %s",
|
714
|
+
response.status,
|
715
|
+
response.reason,
|
716
|
+
)
|
717
|
+
logger.debug("Response body: %s", data)
|
718
|
+
|
719
|
+
except Exception as err:
|
720
|
+
logger.exception("Exception during key prefixes API request: %s", err)
|
721
|
+
|
722
|
+
finally:
|
723
|
+
logger.trace("Closing connection...")
|
724
|
+
conn.close()
|
725
|
+
|
726
|
+
return None
|
727
|
+
|
728
|
+
def cquery(
|
729
|
+
self, query_dict: dict[str, str], max_workers: int = 10
|
730
|
+
) -> Optional[Dict[str, Any]]:
|
731
|
+
"""
|
732
|
+
Execute multiple SOQL queries using the Composite Batch API with threading to reduce network overhead.
|
733
|
+
The function returns a dictionary mapping the original keys to their corresponding batch response.
|
734
|
+
The function requires a dictionary of SOQL queries with keys as logical names (referenceId) and values as SOQL queries.
|
735
|
+
Each query (subrequest) is counted as a unique API request against Salesforce governance limits.
|
736
|
+
|
737
|
+
:param query_dict: A dictionary of SOQL queries with keys as logical names and values as SOQL queries.
|
738
|
+
:param max_workers: The maximum number of threads to spawn for concurrent execution (default is 10).
|
739
|
+
:return: Dict mapping the original keys to their corresponding batch response or None on failure.
|
740
|
+
"""
|
741
|
+
if not query_dict:
|
742
|
+
logger.warning("No queries to execute.")
|
743
|
+
return None
|
744
|
+
|
745
|
+
self._refresh_token_if_needed()
|
746
|
+
|
747
|
+
if not self.access_token:
|
748
|
+
logger.error("No access token available for query.")
|
749
|
+
return None
|
750
|
+
|
751
|
+
def _execute_batch(queries_batch):
|
752
|
+
endpoint = f"/services/data/{self.api_version}/composite/batch"
|
753
|
+
headers = {
|
754
|
+
"Authorization": f"Bearer {self.access_token}",
|
755
|
+
"User-Agent": self.user_agent,
|
756
|
+
"Accept": "application/json",
|
757
|
+
"Content-Type": "application/json",
|
758
|
+
}
|
759
|
+
|
760
|
+
payload = {
|
761
|
+
"haltOnError": False,
|
762
|
+
"batchRequests": [
|
763
|
+
{
|
764
|
+
"method": "GET",
|
765
|
+
"url": f"/services/data/{self.api_version}/query?q={quote(query)}",
|
766
|
+
}
|
767
|
+
for query in queries_batch
|
768
|
+
],
|
769
|
+
}
|
770
|
+
|
771
|
+
parsed_url = urlparse(self.instance_url)
|
772
|
+
conn = self._create_connection(parsed_url.netloc)
|
773
|
+
batch_results = {}
|
774
|
+
|
775
|
+
try:
|
776
|
+
logger.trace("Request endpoint: %s", endpoint)
|
777
|
+
logger.trace("Request headers: %s", headers)
|
778
|
+
logger.trace("Request payload: %s", json.dumps(payload, indent=2))
|
779
|
+
|
780
|
+
conn.request("POST", endpoint, json.dumps(payload), headers=headers)
|
781
|
+
conn.sock.settimeout(60 * 10)
|
782
|
+
response = conn.getresponse()
|
783
|
+
data = response.read().decode("utf-8")
|
784
|
+
self._http_resp_header_logic(response)
|
785
|
+
|
786
|
+
if response.status == 200:
|
787
|
+
logger.debug("Composite query successful.")
|
788
|
+
logger.trace("Composite query full response: %s", data)
|
789
|
+
results = json.loads(data).get("results", [])
|
790
|
+
for i, result in enumerate(results):
|
791
|
+
records = []
|
792
|
+
if "result" in result and "records" in result["result"]:
|
793
|
+
records.extend(result["result"]["records"])
|
794
|
+
# Handle pagination
|
795
|
+
while not result["result"].get("done", True):
|
796
|
+
next_url = result["result"].get("nextRecordsUrl")
|
797
|
+
if next_url:
|
798
|
+
conn.request("GET", next_url, headers=headers)
|
799
|
+
response = conn.getresponse()
|
800
|
+
data = response.read().decode("utf-8")
|
801
|
+
self._http_resp_header_logic(response)
|
802
|
+
if response.status == 200:
|
803
|
+
next_results = json.loads(data)
|
804
|
+
records.extend(next_results.get("records", []))
|
805
|
+
result["result"]["done"] = next_results.get("done")
|
806
|
+
else:
|
807
|
+
logger.error(
|
808
|
+
"Failed to fetch next records: %s",
|
809
|
+
response.reason,
|
810
|
+
)
|
811
|
+
break
|
812
|
+
else:
|
813
|
+
result["result"]["done"] = True
|
814
|
+
paginated_results = result["result"]
|
815
|
+
paginated_results["records"] = records
|
816
|
+
if "nextRecordsUrl" in paginated_results:
|
817
|
+
del paginated_results["nextRecordsUrl"]
|
818
|
+
batch_results[keys[i]] = paginated_results
|
819
|
+
if result.get("statusCode") != 200:
|
820
|
+
logger.error("Query failed for key %s: %s", keys[i], result)
|
821
|
+
logger.error(
|
822
|
+
"Query failed with HTTP status %s (%s)",
|
823
|
+
result.get("statusCode"),
|
824
|
+
result.get("statusMessage"),
|
825
|
+
)
|
826
|
+
logger.trace("Query response: %s", result)
|
827
|
+
else:
|
828
|
+
logger.error(
|
829
|
+
"Composite query failed with HTTP status %s (%s)",
|
830
|
+
response.status,
|
831
|
+
response.reason,
|
832
|
+
)
|
833
|
+
batch_results[keys[i]] = data
|
834
|
+
logger.trace("Composite query response: %s", data)
|
835
|
+
except Exception as err:
|
836
|
+
logger.exception("Exception during composite query: %s", err)
|
837
|
+
finally:
|
838
|
+
logger.trace("Closing connection...")
|
839
|
+
conn.close()
|
840
|
+
|
841
|
+
return batch_results
|
842
|
+
|
843
|
+
keys = list(query_dict.keys())
|
844
|
+
results_dict = OrderedDict()
|
845
|
+
|
846
|
+
with ThreadPoolExecutor(max_workers=max_workers) as executor:
|
847
|
+
futures = []
|
848
|
+
for i in range(0, len(keys), 25):
|
849
|
+
batch_keys = keys[i : i + 25]
|
850
|
+
batch_queries = [query_dict[key] for key in batch_keys]
|
851
|
+
futures.append(executor.submit(_execute_batch, batch_queries))
|
852
|
+
|
853
|
+
for future in as_completed(futures):
|
854
|
+
results_dict.update(future.result())
|
855
|
+
|
856
|
+
logger.trace("Composite query results: %s", results_dict)
|
857
|
+
return results_dict
|
858
|
+
|
606
859
|
def _reconnect_with_backoff(self, attempt: int) -> None:
|
607
860
|
wait_time = min(2**attempt, 60)
|
608
861
|
logger.warning(
|
609
862
|
f"Reconnecting after failure, backoff {wait_time}s (attempt {attempt})"
|
610
863
|
)
|
611
864
|
time.sleep(wait_time)
|
612
|
-
self._refresh_token_if_needed()
|
613
865
|
|
614
866
|
def _subscribe_topic(
|
615
867
|
self,
|
sfq-0.0.11/PKG-INFO
DELETED
@@ -1,133 +0,0 @@
|
|
1
|
-
Metadata-Version: 2.4
|
2
|
-
Name: sfq
|
3
|
-
Version: 0.0.11
|
4
|
-
Summary: Python wrapper for the Salesforce's Query API.
|
5
|
-
Author-email: David Moruzzi <sfq.pypi@dmoruzi.com>
|
6
|
-
Keywords: salesforce,salesforce query
|
7
|
-
Classifier: Development Status :: 3 - Alpha
|
8
|
-
Classifier: Intended Audience :: Developers
|
9
|
-
Classifier: Programming Language :: Python :: 3.9
|
10
|
-
Classifier: Programming Language :: Python :: 3.10
|
11
|
-
Classifier: Programming Language :: Python :: 3.12
|
12
|
-
Classifier: Programming Language :: Python :: 3.13
|
13
|
-
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
14
|
-
Requires-Python: >=3.9
|
15
|
-
Description-Content-Type: text/markdown
|
16
|
-
|
17
|
-
# sfq (Salesforce Query)
|
18
|
-
|
19
|
-
sfq is a lightweight Python wrapper library designed to simplify querying Salesforce, reducing repetitive code for accessing Salesforce data.
|
20
|
-
|
21
|
-
For more varied workflows, consider using an alternative like [Simple Salesforce](https://simple-salesforce.readthedocs.io/en/stable/). This library was even referenced on the [Salesforce Developers Blog](https://developer.salesforce.com/blogs/2021/09/how-to-automate-data-extraction-from-salesforce-using-python).
|
22
|
-
|
23
|
-
## Features
|
24
|
-
|
25
|
-
- Simplified query execution for Salesforce instances.
|
26
|
-
- Integration with Salesforce authentication via refresh tokens.
|
27
|
-
- Option to interact with Salesforce Tooling API for more advanced queries.
|
28
|
-
|
29
|
-
## Installation
|
30
|
-
|
31
|
-
You can install the `sfq` library using `pip`:
|
32
|
-
|
33
|
-
```bash
|
34
|
-
pip install sfq
|
35
|
-
```
|
36
|
-
|
37
|
-
## Usage
|
38
|
-
|
39
|
-
### Library Querying
|
40
|
-
|
41
|
-
```python
|
42
|
-
from sfq import SFAuth
|
43
|
-
|
44
|
-
# Initialize the SFAuth class with authentication details
|
45
|
-
sf = SFAuth(
|
46
|
-
instance_url="https://example-dev-ed.trailblaze.my.salesforce.com",
|
47
|
-
client_id="PlatformCLI",
|
48
|
-
refresh_token="your-refresh-token-here"
|
49
|
-
)
|
50
|
-
|
51
|
-
# Execute a query to fetch account records
|
52
|
-
print(sf.query("SELECT Id FROM Account LIMIT 5"))
|
53
|
-
|
54
|
-
# Execute a query to fetch Tooling API data
|
55
|
-
print(sf.query("SELECT Id, FullName, Metadata FROM SandboxSettings LIMIT 5", tooling=True))
|
56
|
-
```
|
57
|
-
|
58
|
-
### Bash Querying
|
59
|
-
|
60
|
-
You can easily incorporate this into ad-hoc bash scripts or commands:
|
61
|
-
|
62
|
-
```bash
|
63
|
-
python -c "from sfq import SFAuth; sf = SFAuth(instance_url='$instance_url', client_id='$client_id', refresh_token='$refresh_token'); print(sf.query('$query'))" | jq -r '.records[].Id'
|
64
|
-
```
|
65
|
-
|
66
|
-
## How to Obtain Salesforce Tokens
|
67
|
-
|
68
|
-
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:
|
69
|
-
|
70
|
-
### Steps to Get Tokens
|
71
|
-
|
72
|
-
1. **Install the Salesforce CLI**:
|
73
|
-
Follow the instructions on the [Salesforce CLI installation page](https://developer.salesforce.com/tools/salesforcecli).
|
74
|
-
|
75
|
-
2. **Authenticate with Salesforce**:
|
76
|
-
Login to your Salesforce org using the following command:
|
77
|
-
|
78
|
-
```bash
|
79
|
-
sf org login web --alias int --instance-url https://corpa--int.sandbox.my.salesforce.com
|
80
|
-
```
|
81
|
-
|
82
|
-
3. **Display Org Details**:
|
83
|
-
To get the client ID, refresh token, and instance URL, run:
|
84
|
-
|
85
|
-
```bash
|
86
|
-
sf org display --target-org int --verbose --json
|
87
|
-
```
|
88
|
-
|
89
|
-
The output will look like this:
|
90
|
-
|
91
|
-
```json
|
92
|
-
{
|
93
|
-
"status": 0,
|
94
|
-
"result": {
|
95
|
-
"id": "00Daa0000000000000",
|
96
|
-
"apiVersion": "63.0",
|
97
|
-
"accessToken": "your-access-token-here",
|
98
|
-
"instanceUrl": "https://example-dev-ed.trailblaze.my.salesforce.com",
|
99
|
-
"username": "user@example.com",
|
100
|
-
"clientId": "PlatformCLI",
|
101
|
-
"connectedStatus": "Connected",
|
102
|
-
"sfdxAuthUrl": "force://PlatformCLI::your-refresh-token-here::https://example-dev-ed.trailblaze.my.salesforce.com",
|
103
|
-
"alias": "int"
|
104
|
-
}
|
105
|
-
}
|
106
|
-
```
|
107
|
-
|
108
|
-
4. **Extract and Use the Tokens**:
|
109
|
-
The `sfdxAuthUrl` is structured as:
|
110
|
-
|
111
|
-
```
|
112
|
-
force://<client_id>::<refresh_token>::<instance_url>
|
113
|
-
```
|
114
|
-
|
115
|
-
You can extract and use the tokens in a bash script like this:
|
116
|
-
|
117
|
-
```bash
|
118
|
-
query="SELECT Id FROM User WHERE IsActive = true AND Profile.Name = 'System Administrator'"
|
119
|
-
|
120
|
-
sfdxAuthUrl=$(sf org display --target-org int --verbose --json | jq -r '.result.sfdxAuthUrl' | sed 's/force:\/\///')
|
121
|
-
clientId=$(echo "$sfdxAuthUrl" | sed 's/::/\n/g' | sed -n '1p')
|
122
|
-
refreshToken=$(echo "$sfdxAuthUrl" | sed 's/::/\n/g' | sed -n '2p')
|
123
|
-
instanceUrl=$(echo "$sfdxAuthUrl" | sed 's/::/\n/g' | sed -n '3p')
|
124
|
-
|
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'
|
126
|
-
```
|
127
|
-
|
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.
|
133
|
-
|
sfq-0.0.11/README.md
DELETED
@@ -1,117 +0,0 @@
|
|
1
|
-
# sfq (Salesforce Query)
|
2
|
-
|
3
|
-
sfq is a lightweight Python wrapper library designed to simplify querying Salesforce, reducing repetitive code for accessing Salesforce data.
|
4
|
-
|
5
|
-
For more varied workflows, consider using an alternative like [Simple Salesforce](https://simple-salesforce.readthedocs.io/en/stable/). This library was even referenced on the [Salesforce Developers Blog](https://developer.salesforce.com/blogs/2021/09/how-to-automate-data-extraction-from-salesforce-using-python).
|
6
|
-
|
7
|
-
## Features
|
8
|
-
|
9
|
-
- Simplified query execution for Salesforce instances.
|
10
|
-
- Integration with Salesforce authentication via refresh tokens.
|
11
|
-
- Option to interact with Salesforce Tooling API for more advanced queries.
|
12
|
-
|
13
|
-
## Installation
|
14
|
-
|
15
|
-
You can install the `sfq` library using `pip`:
|
16
|
-
|
17
|
-
```bash
|
18
|
-
pip install sfq
|
19
|
-
```
|
20
|
-
|
21
|
-
## Usage
|
22
|
-
|
23
|
-
### Library Querying
|
24
|
-
|
25
|
-
```python
|
26
|
-
from sfq import SFAuth
|
27
|
-
|
28
|
-
# Initialize the SFAuth class with authentication details
|
29
|
-
sf = SFAuth(
|
30
|
-
instance_url="https://example-dev-ed.trailblaze.my.salesforce.com",
|
31
|
-
client_id="PlatformCLI",
|
32
|
-
refresh_token="your-refresh-token-here"
|
33
|
-
)
|
34
|
-
|
35
|
-
# Execute a query to fetch account records
|
36
|
-
print(sf.query("SELECT Id FROM Account LIMIT 5"))
|
37
|
-
|
38
|
-
# Execute a query to fetch Tooling API data
|
39
|
-
print(sf.query("SELECT Id, FullName, Metadata FROM SandboxSettings LIMIT 5", tooling=True))
|
40
|
-
```
|
41
|
-
|
42
|
-
### Bash Querying
|
43
|
-
|
44
|
-
You can easily incorporate this into ad-hoc bash scripts or commands:
|
45
|
-
|
46
|
-
```bash
|
47
|
-
python -c "from sfq import SFAuth; sf = SFAuth(instance_url='$instance_url', client_id='$client_id', refresh_token='$refresh_token'); print(sf.query('$query'))" | jq -r '.records[].Id'
|
48
|
-
```
|
49
|
-
|
50
|
-
## How to Obtain Salesforce Tokens
|
51
|
-
|
52
|
-
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:
|
53
|
-
|
54
|
-
### Steps to Get Tokens
|
55
|
-
|
56
|
-
1. **Install the Salesforce CLI**:
|
57
|
-
Follow the instructions on the [Salesforce CLI installation page](https://developer.salesforce.com/tools/salesforcecli).
|
58
|
-
|
59
|
-
2. **Authenticate with Salesforce**:
|
60
|
-
Login to your Salesforce org using the following command:
|
61
|
-
|
62
|
-
```bash
|
63
|
-
sf org login web --alias int --instance-url https://corpa--int.sandbox.my.salesforce.com
|
64
|
-
```
|
65
|
-
|
66
|
-
3. **Display Org Details**:
|
67
|
-
To get the client ID, refresh token, and instance URL, run:
|
68
|
-
|
69
|
-
```bash
|
70
|
-
sf org display --target-org int --verbose --json
|
71
|
-
```
|
72
|
-
|
73
|
-
The output will look like this:
|
74
|
-
|
75
|
-
```json
|
76
|
-
{
|
77
|
-
"status": 0,
|
78
|
-
"result": {
|
79
|
-
"id": "00Daa0000000000000",
|
80
|
-
"apiVersion": "63.0",
|
81
|
-
"accessToken": "your-access-token-here",
|
82
|
-
"instanceUrl": "https://example-dev-ed.trailblaze.my.salesforce.com",
|
83
|
-
"username": "user@example.com",
|
84
|
-
"clientId": "PlatformCLI",
|
85
|
-
"connectedStatus": "Connected",
|
86
|
-
"sfdxAuthUrl": "force://PlatformCLI::your-refresh-token-here::https://example-dev-ed.trailblaze.my.salesforce.com",
|
87
|
-
"alias": "int"
|
88
|
-
}
|
89
|
-
}
|
90
|
-
```
|
91
|
-
|
92
|
-
4. **Extract and Use the Tokens**:
|
93
|
-
The `sfdxAuthUrl` is structured as:
|
94
|
-
|
95
|
-
```
|
96
|
-
force://<client_id>::<refresh_token>::<instance_url>
|
97
|
-
```
|
98
|
-
|
99
|
-
You can extract and use the tokens in a bash script like this:
|
100
|
-
|
101
|
-
```bash
|
102
|
-
query="SELECT Id FROM User WHERE IsActive = true AND Profile.Name = 'System Administrator'"
|
103
|
-
|
104
|
-
sfdxAuthUrl=$(sf org display --target-org int --verbose --json | jq -r '.result.sfdxAuthUrl' | sed 's/force:\/\///')
|
105
|
-
clientId=$(echo "$sfdxAuthUrl" | sed 's/::/\n/g' | sed -n '1p')
|
106
|
-
refreshToken=$(echo "$sfdxAuthUrl" | sed 's/::/\n/g' | sed -n '2p')
|
107
|
-
instanceUrl=$(echo "$sfdxAuthUrl" | sed 's/::/\n/g' | sed -n '3p')
|
108
|
-
|
109
|
-
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'
|
110
|
-
```
|
111
|
-
|
112
|
-
## Important Considerations
|
113
|
-
|
114
|
-
- **Security**: Safeguard your refresh token diligently, as it provides access to your Salesforce environment. Avoid sharing or exposing it in unsecured locations.
|
115
|
-
- **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.
|
116
|
-
- **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.
|
117
|
-
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|