sfq 0.0.16__tar.gz → 0.0.18__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.18/.github/workflows/publish.yml +106 -0
- {sfq-0.0.16 → sfq-0.0.18}/PKG-INFO +14 -14
- {sfq-0.0.16 → sfq-0.0.18}/README.md +13 -13
- {sfq-0.0.16 → sfq-0.0.18}/pyproject.toml +1 -1
- {sfq-0.0.16 → sfq-0.0.18}/src/sfq/__init__.py +86 -8
- {sfq-0.0.16 → sfq-0.0.18}/uv.lock +1 -1
- sfq-0.0.16/.github/workflows/publish.yml +0 -37
- {sfq-0.0.16 → sfq-0.0.18}/.gitignore +0 -0
- {sfq-0.0.16 → sfq-0.0.18}/.python-version +0 -0
- {sfq-0.0.16 → sfq-0.0.18}/src/sfq/_cometd.py +0 -0
- {sfq-0.0.16 → sfq-0.0.18}/src/sfq/py.typed +0 -0
@@ -0,0 +1,106 @@
|
|
1
|
+
name: Version, Build, and Publish
|
2
|
+
|
3
|
+
on:
|
4
|
+
push:
|
5
|
+
branches: ["main"]
|
6
|
+
workflow_dispatch:
|
7
|
+
|
8
|
+
permissions:
|
9
|
+
contents: write
|
10
|
+
id-token: write
|
11
|
+
|
12
|
+
jobs:
|
13
|
+
release:
|
14
|
+
runs-on: ubuntu-latest
|
15
|
+
environment: release
|
16
|
+
|
17
|
+
steps:
|
18
|
+
- name: Checkout repo
|
19
|
+
uses: actions/checkout@v4
|
20
|
+
|
21
|
+
- name: Set up Python 3.12
|
22
|
+
uses: actions/setup-python@v5
|
23
|
+
with:
|
24
|
+
python-version: 3.12
|
25
|
+
|
26
|
+
- name: Install uv CLI
|
27
|
+
uses: astral-sh/setup-uv@v4
|
28
|
+
with:
|
29
|
+
version: latest
|
30
|
+
enable-cache: true
|
31
|
+
|
32
|
+
- name: Sync dependencies with uv
|
33
|
+
run: uv sync
|
34
|
+
|
35
|
+
- name: Authenticate GitHub CLI
|
36
|
+
run: |
|
37
|
+
echo "${{ secrets.GITHUB_TOKEN }}" | gh auth login --with-token
|
38
|
+
|
39
|
+
- name: Calculate next patch version from latest tag (using gh)
|
40
|
+
id: get_version
|
41
|
+
run: |
|
42
|
+
echo "Fetching tags using gh CLI..."
|
43
|
+
|
44
|
+
TAGS=$(gh api repos/${{ github.repository }}/tags --jq '.[].name' | grep -E '^[0-9]+\.[0-9]+\.[0-9]+$' | sort -V)
|
45
|
+
|
46
|
+
echo "All semver tags found:"
|
47
|
+
echo "$TAGS"
|
48
|
+
|
49
|
+
LATEST_TAG=$(echo "$TAGS" | tail -n1)
|
50
|
+
echo "Latest tag found: $LATEST_TAG"
|
51
|
+
|
52
|
+
if [ -z "$LATEST_TAG" ]; then
|
53
|
+
NEXT_VERSION="0.0.1"
|
54
|
+
echo "No tags found, starting at $NEXT_VERSION"
|
55
|
+
else
|
56
|
+
IFS='.' read -r MAJOR MINOR PATCH <<< "$LATEST_TAG"
|
57
|
+
PATCH=$((PATCH + 1))
|
58
|
+
NEXT_VERSION="$MAJOR.$MINOR.$PATCH"
|
59
|
+
echo "Incremented patch version to $NEXT_VERSION"
|
60
|
+
fi
|
61
|
+
|
62
|
+
echo "version=$NEXT_VERSION" >> $GITHUB_OUTPUT
|
63
|
+
|
64
|
+
- name: Update version strings in files
|
65
|
+
run: |
|
66
|
+
VERSION=${{ steps.get_version.outputs.version }}
|
67
|
+
|
68
|
+
echo "Updating pyproject.toml version to $VERSION"
|
69
|
+
sed -i -E "s/(^version *= *\")([0-9]+\.[0-9]+\.[0-9]+)(\")/\1$VERSION\3/" pyproject.toml
|
70
|
+
|
71
|
+
echo "Updating uv.lock version to $VERSION"
|
72
|
+
sed -i -E "s/(^version *= *\")([0-9]+\.[0-9]+\.[0-9]+)(\")/\1$VERSION\3/" uv.lock
|
73
|
+
|
74
|
+
echo "Updating src/sfq/__init__.py user_agent to $VERSION"
|
75
|
+
sed -i -E "s/(user_agent: str = \"sfq\/)[0-9]+\.[0-9]+\.[0-9]+(\")/\1$VERSION\2/" src/sfq/__init__.py
|
76
|
+
sed -i -E "s/(default is \"sfq\/)[0-9]+\.[0-9]+\.[0-9]+(\")/\1$VERSION\2/" src/sfq/__init__.py
|
77
|
+
|
78
|
+
- name: Commit version updates
|
79
|
+
run: |
|
80
|
+
git config user.name "github-actions"
|
81
|
+
git config user.email "github-actions@users.noreply.github.com"
|
82
|
+
git add pyproject.toml uv.lock src/sfq/__init__.py
|
83
|
+
git commit -m "chore: bump version to ${{ steps.get_version.outputs.version }}"
|
84
|
+
git push
|
85
|
+
|
86
|
+
- name: Create and push git tag
|
87
|
+
run: |
|
88
|
+
VERSION=${{ steps.get_version.outputs.version }}
|
89
|
+
git tag $VERSION
|
90
|
+
git push origin $VERSION
|
91
|
+
|
92
|
+
- name: Build package with uv
|
93
|
+
run: uv build
|
94
|
+
|
95
|
+
- name: Publish package distributions to PyPI
|
96
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
97
|
+
|
98
|
+
- name: Upload artifacts to GitHub Release (prerelease)
|
99
|
+
uses: softprops/action-gh-release@v2
|
100
|
+
with:
|
101
|
+
tag_name: ${{ steps.get_version.outputs.version }}
|
102
|
+
name: Release ${{ steps.get_version.outputs.version }}
|
103
|
+
files: dist/*
|
104
|
+
prerelease: true
|
105
|
+
env:
|
106
|
+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: sfq
|
3
|
-
Version: 0.0.
|
3
|
+
Version: 0.0.18
|
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': '
|
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:
|
@@ -40,18 +40,6 @@ print(sf.query("SELECT Id FROM Account LIMIT 5"))
|
|
40
40
|
print(sf.tooling_query("SELECT Id, FullName, Metadata FROM SandboxSettings LIMIT 5"))
|
41
41
|
```
|
42
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
43
|
### Composite Batch Queries
|
56
44
|
|
57
45
|
```python
|
@@ -78,7 +66,7 @@ for subrequest_identifer, subrequest_response in batched_response.items():
|
|
78
66
|
|
79
67
|
```python
|
80
68
|
response = sf.cdelete(['07La0000000bYgj', '07La0000000bYgk', '07La0000000bYgl'])
|
81
|
-
>>> [{'id': '
|
69
|
+
>>> [{'id': '07La0000000bYgj', 'success': True, 'errors': []}, {'id': '07La0000000bYgk', 'success': True, 'errors': []}, {'id': '07La0000000bYgl', 'success': True, 'errors': []}]
|
82
70
|
```
|
83
71
|
|
84
72
|
### Static Resources
|
@@ -94,6 +82,18 @@ print(f'Updated resource: {page}')
|
|
94
82
|
sf.update_static_resource_id('081aj000009jUMXAA2', '<h1>It works!</h1>')
|
95
83
|
```
|
96
84
|
|
85
|
+
### sObject Key Prefixes
|
86
|
+
|
87
|
+
```python
|
88
|
+
# Key prefix via IDs
|
89
|
+
print(sf.get_sobject_prefixes())
|
90
|
+
>>> {'0Pp': 'AIApplication', '6S9': 'AIApplicationConfig', '9qd': 'AIInsightAction', '9bq': 'AIInsightFeedback', '0T2': 'AIInsightReason', '9qc': 'AIInsightValue', ...}
|
91
|
+
|
92
|
+
# Key prefix via names
|
93
|
+
print(sf.get_sobject_prefixes(key_type="name"))
|
94
|
+
>>> {'AIApplication': '0Pp', 'AIApplicationConfig': '6S9', 'AIInsightAction': '9qd', 'AIInsightFeedback': '9bq', 'AIInsightReason': '0T2', 'AIInsightValue': '9qc', ...}
|
95
|
+
```
|
96
|
+
|
97
97
|
## How to Obtain Salesforce Tokens
|
98
98
|
|
99
99
|
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:
|
@@ -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.
|
87
|
+
user_agent: str = "sfq/0.0.18",
|
88
88
|
sforce_client: str = "_auto",
|
89
|
-
proxy: str = "
|
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.
|
103
|
+
:param user_agent: Custom User-Agent string (default is "sfq/0.0.18").
|
104
104
|
:param sforce_client: Custom Application Identifier (default is user_agent).
|
105
|
-
:param proxy: The proxy configuration, "
|
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 =
|
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 =
|
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 == "
|
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,37 +0,0 @@
|
|
1
|
-
name: Publish to PyPI
|
2
|
-
|
3
|
-
on:
|
4
|
-
release:
|
5
|
-
types: [published]
|
6
|
-
workflow_dispatch:
|
7
|
-
|
8
|
-
jobs:
|
9
|
-
|
10
|
-
publish-release:
|
11
|
-
name: upload release to PyPI
|
12
|
-
runs-on: ubuntu-latest
|
13
|
-
permissions:
|
14
|
-
id-token: write
|
15
|
-
environment: release
|
16
|
-
steps:
|
17
|
-
- name: Check out repository
|
18
|
-
uses: actions/checkout@v4
|
19
|
-
|
20
|
-
- uses: actions/setup-python@v5
|
21
|
-
with:
|
22
|
-
python-version: "3.12"
|
23
|
-
|
24
|
-
- name: Install uv
|
25
|
-
uses: astral-sh/setup-uv@v4
|
26
|
-
with:
|
27
|
-
version: "latest"
|
28
|
-
enable-cache: true
|
29
|
-
|
30
|
-
- name: Uv sync
|
31
|
-
run: uv sync
|
32
|
-
|
33
|
-
- name: Build package
|
34
|
-
run: uv build
|
35
|
-
|
36
|
-
- name: Publish package distributions to PyPI
|
37
|
-
uses: pypa/gh-action-pypi-publish@release/v1
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|