sfq 0.0.21__tar.gz → 0.0.23__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.
@@ -32,6 +32,9 @@ jobs:
32
32
  - name: Sync dependencies with uv
33
33
  run: uv sync
34
34
 
35
+ - name: Install pytest
36
+ run: pip install pytest
37
+
35
38
  - name: Authenticate GitHub CLI
36
39
  run: |
37
40
  echo "${{ secrets.GITHUB_TOKEN }}" | gh auth login --with-token
@@ -75,12 +78,20 @@ jobs:
75
78
  sed -i -E "s/(user_agent: str = \"sfq\/)[0-9]+\.[0-9]+\.[0-9]+(\")/\1$VERSION\2/" src/sfq/__init__.py
76
79
  sed -i -E "s/(default is \"sfq\/)[0-9]+\.[0-9]+\.[0-9]+(\")/\1$VERSION\2/" src/sfq/__init__.py
77
80
 
81
+ - name: Run tests
82
+ run: pytest --verbose --strict-config
83
+ env:
84
+ SF_INSTANCE_URL: ${{ secrets.SF_INSTANCE_URL }}
85
+ SF_CLIENT_ID: ${{ secrets.SF_CLIENT_ID }}
86
+ SF_CLIENT_SECRET: ${{ secrets.SF_CLIENT_SECRET }}
87
+ SF_REFRESH_TOKEN: ${{ secrets.SF_REFRESH_TOKEN }}
88
+
78
89
  - name: Commit version updates
79
90
  run: |
80
- git config user.name "github-actions"
81
- git config user.email "github-actions@users.noreply.github.com"
91
+ git config user.name "github-actions[bot]"
92
+ git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
82
93
  git add pyproject.toml uv.lock src/sfq/__init__.py
83
- git commit -m "chore: bump version to ${{ steps.get_version.outputs.version }}"
94
+ git commit -m "CI: bump version to ${{ steps.get_version.outputs.version }}"
84
95
  git push
85
96
 
86
97
  - name: Create and push git tag
@@ -104,3 +115,48 @@ jobs:
104
115
  prerelease: true
105
116
  env:
106
117
  GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
118
+
119
+ - name: Install pdoc
120
+ run: pip install pdoc
121
+
122
+ - name: Generate docs
123
+ run: pdoc .\src\sfq\__init__.py -o docs --no-search
124
+
125
+ - name: Set sfq.html as default index page
126
+ run: |
127
+ rm docs/index.html
128
+ mv docs/sfq.html docs/index.html
129
+
130
+
131
+ - name: Prepare docs branch (first-time safe)
132
+ run: |
133
+ git config user.name "github-actions[bot]"
134
+ git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
135
+
136
+ git fetch origin
137
+ rm -rf docs-branch
138
+
139
+ if git show-ref --verify --quiet refs/remotes/origin/docs; then
140
+ echo "Branch 'docs' exists. Checking it out with worktree..."
141
+ git worktree add -B docs docs-branch origin/docs
142
+ else
143
+ echo "Branch 'docs' does not exist. Creating new orphan branch..."
144
+ mkdir docs-branch
145
+ cd docs-branch
146
+ git init
147
+ git checkout --orphan docs
148
+ git commit --allow-empty -m "CI: Initial empty commit"
149
+ git remote add origin https://github.com/${{ github.repository }}.git
150
+ git push origin docs
151
+ cd ..
152
+ git worktree add -B docs docs-branch origin/docs
153
+ fi
154
+
155
+ - name: Push generated docs to docs branch
156
+ run: |
157
+ rm -rf docs-branch/*
158
+ cp -r docs/* docs-branch/
159
+ cd docs-branch
160
+ git add .
161
+ git commit -m "CI: Update documentation" || echo "No changes to commit"
162
+ git push origin docs --force
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sfq
3
- Version: 0.0.21
3
+ Version: 0.0.23
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
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "sfq"
3
- version = "0.0.21"
3
+ version = "0.0.23"
4
4
  description = "Python wrapper for the Salesforce's Query API."
5
5
  readme = "README.md"
6
6
  authors = [{ name = "David Moruzzi", email = "sfq.pypi@dmoruzi.com" }]
@@ -1,3 +1,7 @@
1
+ """
2
+ .. include:: ../../README.md
3
+ """
4
+
1
5
  import base64
2
6
  import http.client
3
7
  import json
@@ -10,6 +14,8 @@ from concurrent.futures import ThreadPoolExecutor, as_completed
10
14
  from typing import Any, Dict, Iterable, Literal, Optional, List, Tuple
11
15
  from urllib.parse import quote, urlparse
12
16
 
17
+ __all__ = ["SFAuth"] # https://pdoc.dev/docs/pdoc.html#control-what-is-documented
18
+
13
19
  TRACE = 5
14
20
  logging.addLevelName(TRACE, "TRACE")
15
21
 
@@ -84,7 +90,7 @@ class SFAuth:
84
90
  access_token: Optional[str] = None,
85
91
  token_expiration_time: Optional[float] = None,
86
92
  token_lifetime: int = 15 * 60,
87
- user_agent: str = "sfq/0.0.21",
93
+ user_agent: str = "sfq/0.0.23",
88
94
  sforce_client: str = "_auto",
89
95
  proxy: str = "_auto",
90
96
  ) -> None:
@@ -94,27 +100,145 @@ class SFAuth:
94
100
  :param instance_url: The Salesforce instance URL.
95
101
  :param client_id: The client ID for OAuth.
96
102
  :param refresh_token: The refresh token for OAuth.
97
- :param client_secret: The client secret for OAuth (default is "_deprecation_warning").
98
- :param api_version: The Salesforce API version (default is "v64.0").
99
- :param token_endpoint: The token endpoint (default is "/services/oauth2/token").
100
- :param access_token: The access token for the current session (default is None).
101
- :param token_expiration_time: The expiration time of the access token (default is None).
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.21").
104
- :param sforce_client: Custom Application Identifier (default is user_agent).
105
- :param proxy: The proxy configuration, "_auto" to use environment (default is "_auto").
103
+ :param client_secret: The client secret for OAuth.
104
+ :param api_version: The Salesforce API version.
105
+ :param token_endpoint: The token endpoint.
106
+ :param access_token: The access token for the current session.
107
+ :param token_expiration_time: The expiration time of the access token.
108
+ :param token_lifetime: The lifetime of the access token in seconds.
109
+ :param user_agent: Custom User-Agent string.
110
+ :param sforce_client: Custom Application Identifier.
111
+ :param proxy: The proxy configuration, "_auto" to use environment.
106
112
  """
107
113
  self.instance_url = self._format_instance_url(instance_url)
114
+ """
115
+ ### `instance_url`
116
+ **The fully qualified Salesforce instance URL.**
117
+
118
+ - Should end with `.my.salesforce.com`
119
+ - No trailing slash
120
+
121
+ **Examples:**
122
+ - `https://sfq-dev-ed.trailblazer.my.salesforce.com`
123
+ - `https://sfq.my.salesforce.com`
124
+ - `https://sfq--dev.sandbox.my.salesforce.com`
125
+ """
126
+
108
127
  self.client_id = client_id
128
+ """
129
+ ### `client_id`
130
+ **The OAuth client ID.**
131
+
132
+ - Uniquely identifies your **Connected App** in Salesforce
133
+ - If using **Salesforce CLI**, this is `"PlatformCLI"`
134
+ - For other apps, find this value in the **Connected App details**
135
+ """
136
+
109
137
  self.client_secret = client_secret
138
+ """
139
+ ### `client_secret`
140
+ **The OAuth client secret.**
141
+
142
+ - Secret key associated with your Connected App
143
+ - For **Salesforce CLI**, this is typically an empty string `""`
144
+ - For custom apps, locate it in the **Connected App settings**
145
+ """
146
+
110
147
  self.refresh_token = refresh_token
148
+ """
149
+ ### `refresh_token`
150
+ **The OAuth refresh token.**
151
+
152
+ - Used to fetch new access tokens when the current one expires
153
+ - For CLI, obtain via:
154
+
155
+ ```bash
156
+ sf org display --json
157
+ ````
158
+
159
+ * For other apps, this value is returned during the **OAuth authorization flow**
160
+ * 📖 [Salesforce OAuth Flows Documentation](https://help.salesforce.com/s/articleView?id=xcloud.remoteaccess_oauth_flows.htm&type=5)
161
+ """
162
+
111
163
  self.api_version = api_version
164
+ """
165
+
166
+ ### `api_version`
167
+
168
+ **The Salesforce API version to use.**
169
+
170
+ * Must include the `"v"` prefix (e.g., `"v64.0"`)
171
+ * Periodically updated to align with new Salesforce releases
172
+ """
173
+
112
174
  self.token_endpoint = token_endpoint
175
+ """
176
+
177
+ ### `token_endpoint`
178
+
179
+ **The token URL path for OAuth authentication.**
180
+
181
+ * Defaults to Salesforce's `.well-known/openid-configuration` for *token* endpoint
182
+ * Should start with a **leading slash**, e.g., `/services/oauth2/token`
183
+ * No customization is typical, but internal designs may use custom ApexRest endpoints
184
+ """
185
+
113
186
  self.access_token = access_token
187
+ """
188
+
189
+ ### `access_token`
190
+
191
+ **The current OAuth access token.**
192
+
193
+ * Used to authorize API requests
194
+ * Does not include Bearer prefix, strictly the token
195
+ """
196
+
114
197
  self.token_expiration_time = token_expiration_time
198
+ """
199
+
200
+ ### `token_expiration_time`
201
+
202
+ **Unix timestamp (in seconds) for access token expiration.**
203
+
204
+ * Managed automatically by the library
205
+ * Useful for checking when to refresh the token
206
+ """
207
+
115
208
  self.token_lifetime = token_lifetime
209
+ """
210
+
211
+ ### `token_lifetime`
212
+
213
+ **Access token lifespan in seconds.**
214
+
215
+ * Determined by your Connected App's session policies
216
+ * Used to calculate when to refresh the token
217
+ """
218
+
116
219
  self.user_agent = user_agent
220
+ """
221
+
222
+ ### `user_agent`
223
+
224
+ **Custom User-Agent string for API calls.**
225
+
226
+ * Included in HTTP request headers
227
+ * Useful for identifying traffic in Salesforce `ApiEvent` logs
228
+ """
229
+
117
230
  self.sforce_client = str(sforce_client).replace(",", "")
231
+ """
232
+
233
+ ### `sforce_client`
234
+
235
+ **Custom application identifier.**
236
+
237
+ * Included in the `Sforce-Call-Options` header
238
+ * Useful for identifying traffic in Event Log Files
239
+ * Commas are not allowed; will be stripped
240
+ """
241
+
118
242
  self._auto_configure_proxy(proxy)
119
243
  self._high_api_usage_threshold = 80
120
244
 
@@ -0,0 +1,16 @@
1
+ # tests/conftest.py
2
+
3
+ executed_tests = 0
4
+
5
+ def pytest_runtest_logreport(report):
6
+ global executed_tests
7
+ if report.when == "call" and report.passed:
8
+ executed_tests += 1
9
+ elif report.when == "call" and report.failed:
10
+ executed_tests += 1
11
+
12
+ def pytest_sessionfinish(session, exitstatus):
13
+ if session.testscollected > 0 and executed_tests == 0:
14
+ print()
15
+ print("❌ Pytest collected tests, but all were skipped. Failing the run.")
16
+ session.exitstatus = 1
@@ -0,0 +1,44 @@
1
+ import os
2
+ import sys
3
+ from pathlib import Path
4
+
5
+ import pytest
6
+
7
+ # --- Setup local import path ---
8
+ project_root = Path(__file__).resolve().parents[1]
9
+ src_path = project_root / "src"
10
+ sys.path.insert(0, str(src_path))
11
+ from sfq import SFAuth # noqa: E402
12
+
13
+
14
+ @pytest.fixture(scope="module")
15
+ def sf_instance():
16
+ required_env_vars = [
17
+ "SF_INSTANCE_URL",
18
+ "SF_CLIENT_ID",
19
+ "SF_CLIENT_SECRET",
20
+ "SF_REFRESH_TOKEN",
21
+ ]
22
+
23
+ missing_vars = [var for var in required_env_vars if not os.getenv(var)]
24
+ if missing_vars:
25
+ pytest.skip(f"Missing required env vars: {', '.join(missing_vars)}")
26
+
27
+ sf = SFAuth(
28
+ instance_url=os.getenv("SF_INSTANCE_URL"),
29
+ client_id=os.getenv("SF_CLIENT_ID"),
30
+ client_secret=os.getenv("SF_CLIENT_SECRET"),
31
+ refresh_token=os.getenv("SF_REFRESH_TOKEN"),
32
+ )
33
+ return sf
34
+
35
+
36
+ def test_limits_api(sf_instance):
37
+ """
38
+ Test the limits API endpoint.
39
+ """
40
+
41
+ limits = sf_instance.limits()
42
+
43
+ assert isinstance(limits, dict)
44
+ assert "DailyApiRequests" in limits.keys()
@@ -0,0 +1,79 @@
1
+ import logging
2
+ import os
3
+ import sys
4
+ from io import StringIO
5
+ from pathlib import Path
6
+
7
+ import pytest
8
+
9
+ # --- Setup local import path ---
10
+ project_root = Path(__file__).resolve().parents[1]
11
+ src_path = project_root / "src"
12
+ sys.path.insert(0, str(src_path))
13
+ from sfq import SFAuth # noqa: E402
14
+
15
+
16
+ @pytest.fixture(scope="module")
17
+ def sf_instance():
18
+ required_env_vars = [
19
+ "SF_INSTANCE_URL",
20
+ "SF_CLIENT_ID",
21
+ "SF_CLIENT_SECRET",
22
+ "SF_REFRESH_TOKEN",
23
+ ]
24
+
25
+ missing_vars = [var for var in required_env_vars if not os.getenv(var)]
26
+ if missing_vars:
27
+ pytest.skip(f"Missing required env vars: {', '.join(missing_vars)}")
28
+
29
+ sf = SFAuth(
30
+ instance_url=os.getenv("SF_INSTANCE_URL"),
31
+ client_id=os.getenv("SF_CLIENT_ID"),
32
+ client_secret=os.getenv("SF_CLIENT_SECRET"),
33
+ refresh_token=os.getenv("SF_REFRESH_TOKEN"),
34
+ )
35
+ return sf
36
+
37
+
38
+ @pytest.fixture
39
+ def capture_logs():
40
+ """
41
+ Fixture to capture logs emitted to 'sfq' logger at TRACE level.
42
+ """
43
+ log_stream = StringIO()
44
+ handler = logging.StreamHandler(log_stream)
45
+ handler.setLevel(5)
46
+
47
+ logger = logging.getLogger("sfq")
48
+ original_level = logger.level
49
+ original_handlers = logger.handlers[:]
50
+
51
+ logger.setLevel(5)
52
+ for h in original_handlers:
53
+ logger.removeHandler(h)
54
+ logger.addHandler(handler)
55
+
56
+ yield logger, log_stream
57
+
58
+ # Teardown - restore original handlers and level
59
+ logger.removeHandler(handler)
60
+ for h in original_handlers:
61
+ logger.addHandler(h)
62
+ logger.setLevel(original_level)
63
+
64
+
65
+ def test_access_token_redacted_in_logs(sf_instance, capture_logs):
66
+ """
67
+ Ensure access tokens are redacted in log output to prevent leakage.
68
+ """
69
+ logger, log_stream = capture_logs
70
+
71
+ sf_instance._get_common_headers()
72
+
73
+ logger.handlers[0].flush()
74
+ log_contents = log_stream.getvalue()
75
+
76
+ assert "access_token" in log_contents, "Expected access_token key in logs"
77
+ assert "'access_token': '********'," in log_contents in log_contents, (
78
+ "Access token was not properly redacted in logs"
79
+ )
@@ -3,5 +3,5 @@ requires-python = ">=3.9"
3
3
 
4
4
  [[package]]
5
5
  name = "sfq"
6
- version = "0.0.21"
6
+ version = "0.0.23"
7
7
  source = { editable = "." }
File without changes
File without changes
File without changes
File without changes
File without changes