sfq 0.0.31__py3-none-any.whl → 0.0.33__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 +223 -886
- sfq/_cometd.py +7 -10
- sfq/auth.py +401 -0
- sfq/crud.py +446 -0
- sfq/exceptions.py +54 -0
- sfq/http_client.py +319 -0
- sfq/query.py +398 -0
- sfq/soap.py +181 -0
- sfq/utils.py +196 -0
- {sfq-0.0.31.dist-info → sfq-0.0.33.dist-info}/METADATA +1 -1
- sfq-0.0.33.dist-info/RECORD +13 -0
- sfq-0.0.31.dist-info/RECORD +0 -6
- {sfq-0.0.31.dist-info → sfq-0.0.33.dist-info}/WHEEL +0 -0
sfq/http_client.py
ADDED
@@ -0,0 +1,319 @@
|
|
1
|
+
"""
|
2
|
+
HTTP client module for the SFQ library.
|
3
|
+
|
4
|
+
This module handles HTTP/HTTPS connection management, request/response handling,
|
5
|
+
proxy support, and unified request processing with logging and error handling.
|
6
|
+
"""
|
7
|
+
|
8
|
+
import http.client
|
9
|
+
import json
|
10
|
+
from typing import Dict, Optional, Tuple
|
11
|
+
|
12
|
+
from .auth import AuthManager
|
13
|
+
from .exceptions import ConfigurationError
|
14
|
+
from .utils import format_headers_for_logging, get_logger, log_api_usage
|
15
|
+
|
16
|
+
logger = get_logger(__name__)
|
17
|
+
|
18
|
+
|
19
|
+
class HTTPClient:
|
20
|
+
"""
|
21
|
+
Manages HTTP/HTTPS connections and requests for Salesforce API communication.
|
22
|
+
|
23
|
+
This class encapsulates all HTTP-related functionality including connection
|
24
|
+
creation, proxy support, request/response handling, and logging. It works
|
25
|
+
in conjunction with AuthManager to provide authenticated API access.
|
26
|
+
"""
|
27
|
+
|
28
|
+
def __init__(
|
29
|
+
self,
|
30
|
+
auth_manager: AuthManager,
|
31
|
+
user_agent: str = "sfq/0.0.33",
|
32
|
+
sforce_client: str = "_auto",
|
33
|
+
high_api_usage_threshold: int = 80,
|
34
|
+
) -> None:
|
35
|
+
"""
|
36
|
+
Initialize the HTTPClient with authentication and configuration.
|
37
|
+
|
38
|
+
:param auth_manager: AuthManager instance for authentication
|
39
|
+
:param user_agent: Custom User-Agent string for requests
|
40
|
+
:param sforce_client: Custom application identifier for Sforce-Call-Options
|
41
|
+
:param high_api_usage_threshold: Threshold for high API usage warnings
|
42
|
+
"""
|
43
|
+
self.auth_manager = auth_manager
|
44
|
+
self.user_agent = user_agent
|
45
|
+
self.sforce_client = str(sforce_client).replace(",", "") # Remove commas
|
46
|
+
self.high_api_usage_threshold = high_api_usage_threshold
|
47
|
+
|
48
|
+
# Auto-configure sforce_client if needed
|
49
|
+
if sforce_client == "_auto":
|
50
|
+
self.sforce_client = user_agent
|
51
|
+
|
52
|
+
def create_connection(self, netloc: str) -> http.client.HTTPConnection:
|
53
|
+
"""
|
54
|
+
Create an HTTP/HTTPS connection with optional proxy support.
|
55
|
+
|
56
|
+
:param netloc: The target host and port (e.g., "example.com:443")
|
57
|
+
:return: An HTTPConnection or HTTPSConnection object
|
58
|
+
:raises ConfigurationError: If proxy configuration is invalid
|
59
|
+
"""
|
60
|
+
proxy_config = self.auth_manager.get_proxy_config()
|
61
|
+
|
62
|
+
if proxy_config:
|
63
|
+
try:
|
64
|
+
proxy_hostname, proxy_port = (
|
65
|
+
self.auth_manager.get_proxy_hostname_and_port()
|
66
|
+
)
|
67
|
+
logger.trace("Using proxy: %s", proxy_config)
|
68
|
+
|
69
|
+
# Create HTTPS connection through proxy
|
70
|
+
conn = http.client.HTTPSConnection(proxy_hostname, proxy_port)
|
71
|
+
conn.set_tunnel(netloc)
|
72
|
+
logger.trace("Using proxy tunnel to %s", netloc)
|
73
|
+
|
74
|
+
return conn
|
75
|
+
|
76
|
+
except Exception as e:
|
77
|
+
raise ConfigurationError(f"Failed to create proxy connection: {e}")
|
78
|
+
else:
|
79
|
+
# Direct HTTPS connection
|
80
|
+
conn = http.client.HTTPSConnection(netloc)
|
81
|
+
logger.trace("Direct connection to %s", netloc)
|
82
|
+
return conn
|
83
|
+
|
84
|
+
def get_common_headers(
|
85
|
+
self, include_auth: bool = True, recursive_call: bool = False
|
86
|
+
) -> Dict[str, str]:
|
87
|
+
"""
|
88
|
+
Generate common headers for API requests.
|
89
|
+
|
90
|
+
:param include_auth: Whether to include Authorization header
|
91
|
+
:param recursive_call: Whether this is a recursive call (for token refresh)
|
92
|
+
:return: Dictionary of common headers
|
93
|
+
"""
|
94
|
+
logger.trace("Generating common headers...")
|
95
|
+
headers = {
|
96
|
+
"User-Agent": self.user_agent,
|
97
|
+
"Sforce-Call-Options": f"client={self.sforce_client}",
|
98
|
+
"Accept": "application/json",
|
99
|
+
"Content-Type": "application/json",
|
100
|
+
}
|
101
|
+
|
102
|
+
if include_auth and not recursive_call:
|
103
|
+
logger.trace("Including auth headers...")
|
104
|
+
# Ensure we have a valid token before adding auth headers
|
105
|
+
if self.auth_manager.needs_token_refresh():
|
106
|
+
# This will be handled by the calling code that has access to token refresh
|
107
|
+
logger.trace("Token refresh needed before adding auth headers")
|
108
|
+
self.refresh_token_and_update_auth()
|
109
|
+
|
110
|
+
if self.auth_manager.access_token:
|
111
|
+
headers.update(self.auth_manager.get_auth_headers())
|
112
|
+
return headers
|
113
|
+
|
114
|
+
def send_request(
|
115
|
+
self,
|
116
|
+
method: str,
|
117
|
+
endpoint: str,
|
118
|
+
headers: Dict[str, str],
|
119
|
+
body: Optional[str] = None,
|
120
|
+
) -> Tuple[Optional[int], Optional[str]]:
|
121
|
+
"""
|
122
|
+
Send an HTTP request with built-in logging and error handling.
|
123
|
+
|
124
|
+
:param method: HTTP method (GET, POST, PATCH, DELETE, etc.)
|
125
|
+
:param endpoint: Target API endpoint path
|
126
|
+
:param headers: HTTP headers dictionary
|
127
|
+
:param body: Optional request body
|
128
|
+
:return: Tuple of (status_code, response_body) or (None, None) on failure
|
129
|
+
"""
|
130
|
+
try:
|
131
|
+
# Get the instance netloc for connection
|
132
|
+
netloc = self.auth_manager.get_instance_netloc()
|
133
|
+
conn = self.create_connection(netloc)
|
134
|
+
|
135
|
+
# Log request details
|
136
|
+
logger.trace("Request method: %s", method)
|
137
|
+
logger.trace("Request endpoint: %s", endpoint)
|
138
|
+
logger.trace("Request headers: %s", headers)
|
139
|
+
if body:
|
140
|
+
logger.trace("Request body: %s", body)
|
141
|
+
|
142
|
+
# Send the request
|
143
|
+
conn.request(method, endpoint, body=body, headers=headers)
|
144
|
+
response = conn.getresponse()
|
145
|
+
|
146
|
+
# Process response headers and extract data
|
147
|
+
self._process_response_headers(response)
|
148
|
+
data = response.read().decode("utf-8")
|
149
|
+
|
150
|
+
# Log response details
|
151
|
+
logger.trace("Response status: %s", response.status)
|
152
|
+
logger.trace("Response body: %s", data)
|
153
|
+
|
154
|
+
return response.status, data
|
155
|
+
|
156
|
+
except Exception as err:
|
157
|
+
logger.exception("HTTP request failed: %s", err)
|
158
|
+
return None, None
|
159
|
+
|
160
|
+
finally:
|
161
|
+
try:
|
162
|
+
logger.trace("Closing connection...")
|
163
|
+
conn.close()
|
164
|
+
except Exception:
|
165
|
+
pass # Ignore connection close errors
|
166
|
+
|
167
|
+
def _process_response_headers(self, response: http.client.HTTPResponse) -> None:
|
168
|
+
"""
|
169
|
+
Process HTTP response headers for logging and API usage tracking.
|
170
|
+
|
171
|
+
:param response: The HTTP response object
|
172
|
+
"""
|
173
|
+
logger.trace(
|
174
|
+
"Response status: %s, reason: %s", response.status, response.reason
|
175
|
+
)
|
176
|
+
|
177
|
+
# Get and log headers (filtered for sensitive data)
|
178
|
+
headers = response.getheaders()
|
179
|
+
filtered_headers = format_headers_for_logging(headers)
|
180
|
+
logger.trace("Response headers: %s", filtered_headers)
|
181
|
+
|
182
|
+
# Process specific headers
|
183
|
+
for key, value in headers:
|
184
|
+
if key == "Sforce-Limit-Info":
|
185
|
+
log_api_usage(value, self.high_api_usage_threshold)
|
186
|
+
|
187
|
+
def send_authenticated_request(
|
188
|
+
self,
|
189
|
+
method: str,
|
190
|
+
endpoint: str,
|
191
|
+
body: Optional[str] = None,
|
192
|
+
additional_headers: Optional[Dict[str, str]] = None,
|
193
|
+
) -> Tuple[Optional[int], Optional[str]]:
|
194
|
+
"""
|
195
|
+
Send an authenticated HTTP request with automatic token refresh.
|
196
|
+
|
197
|
+
This is a convenience method that handles common request patterns
|
198
|
+
with authentication and standard headers.
|
199
|
+
|
200
|
+
:param method: HTTP method
|
201
|
+
:param endpoint: API endpoint path
|
202
|
+
:param body: Optional request body
|
203
|
+
:param additional_headers: Optional additional headers to include
|
204
|
+
:return: Tuple of (status_code, response_body) or (None, None) on failure
|
205
|
+
"""
|
206
|
+
headers = self.get_common_headers(include_auth=True)
|
207
|
+
|
208
|
+
if additional_headers:
|
209
|
+
headers.update(additional_headers)
|
210
|
+
|
211
|
+
return self.send_request(method, endpoint, headers, body)
|
212
|
+
|
213
|
+
def send_token_request(
|
214
|
+
self,
|
215
|
+
payload: Dict[str, str],
|
216
|
+
token_endpoint: str,
|
217
|
+
) -> Tuple[Optional[int], Optional[str]]:
|
218
|
+
"""
|
219
|
+
Send a token refresh request with appropriate headers.
|
220
|
+
|
221
|
+
:param payload: Token request payload
|
222
|
+
:param token_endpoint: Token endpoint path
|
223
|
+
:return: Tuple of (status_code, response_body) or (None, None) on failure
|
224
|
+
"""
|
225
|
+
headers = self.get_common_headers(include_auth=False, recursive_call=True)
|
226
|
+
headers.update(self.auth_manager.get_token_request_headers())
|
227
|
+
|
228
|
+
body = self.auth_manager.format_token_request_body(payload)
|
229
|
+
|
230
|
+
return self.send_request("POST", token_endpoint, headers, body)
|
231
|
+
|
232
|
+
def refresh_token_and_update_auth(self) -> Optional[str]:
|
233
|
+
"""
|
234
|
+
Perform a complete token refresh cycle and update the auth manager.
|
235
|
+
|
236
|
+
This method handles the full token refresh process including:
|
237
|
+
- Preparing the token request payload
|
238
|
+
- Sending the token request
|
239
|
+
- Processing the response and updating auth manager state
|
240
|
+
|
241
|
+
:return: New access token if successful, None if failed
|
242
|
+
"""
|
243
|
+
if not self.auth_manager.needs_token_refresh():
|
244
|
+
return self.auth_manager.access_token
|
245
|
+
|
246
|
+
logger.trace("Access token expired or missing, refreshing...")
|
247
|
+
|
248
|
+
# Prepare token request payload
|
249
|
+
payload = self.auth_manager._prepare_token_payload()
|
250
|
+
|
251
|
+
# Send token request
|
252
|
+
status, data = self.send_token_request(
|
253
|
+
payload, self.auth_manager.token_endpoint
|
254
|
+
)
|
255
|
+
|
256
|
+
if status == 200 and data:
|
257
|
+
try:
|
258
|
+
token_data = json.loads(data)
|
259
|
+
|
260
|
+
# Process the token response through auth manager
|
261
|
+
if self.auth_manager.process_token_response(token_data):
|
262
|
+
logger.trace("Token refresh successful.")
|
263
|
+
return self.auth_manager.access_token
|
264
|
+
else:
|
265
|
+
logger.error("Failed to process token response.")
|
266
|
+
return None
|
267
|
+
|
268
|
+
except json.JSONDecodeError as e:
|
269
|
+
logger.error("Failed to parse token response: %s", e)
|
270
|
+
return None
|
271
|
+
else:
|
272
|
+
if status:
|
273
|
+
logger.error("Token refresh failed: %s", status)
|
274
|
+
logger.debug("Response body: %s", data)
|
275
|
+
else:
|
276
|
+
logger.error("Token refresh request failed completely")
|
277
|
+
return None
|
278
|
+
|
279
|
+
def get_instance_url(self) -> str:
|
280
|
+
"""
|
281
|
+
Get the Salesforce instance URL.
|
282
|
+
|
283
|
+
:return: The instance URL
|
284
|
+
"""
|
285
|
+
return self.auth_manager.instance_url
|
286
|
+
|
287
|
+
def get_api_version(self) -> str:
|
288
|
+
"""
|
289
|
+
Get the API version being used.
|
290
|
+
|
291
|
+
:return: The API version string
|
292
|
+
"""
|
293
|
+
return self.auth_manager.api_version
|
294
|
+
|
295
|
+
def is_connection_healthy(self) -> bool:
|
296
|
+
"""
|
297
|
+
Check if the HTTP client can establish connections.
|
298
|
+
|
299
|
+
This method performs a basic connectivity check without making
|
300
|
+
actual API calls.
|
301
|
+
|
302
|
+
:return: True if connection can be established, False otherwise
|
303
|
+
"""
|
304
|
+
try:
|
305
|
+
netloc = self.auth_manager.get_instance_netloc()
|
306
|
+
conn = self.create_connection(netloc)
|
307
|
+
conn.close()
|
308
|
+
return True
|
309
|
+
except Exception as e:
|
310
|
+
logger.debug("Connection health check failed: %s", e)
|
311
|
+
return False
|
312
|
+
|
313
|
+
def __repr__(self) -> str:
|
314
|
+
"""String representation of HTTPClient for debugging."""
|
315
|
+
return (
|
316
|
+
f"HTTPClient(instance_url='{self.auth_manager.instance_url}', "
|
317
|
+
f"user_agent='{self.user_agent}', "
|
318
|
+
f"proxy={bool(self.auth_manager.get_proxy_config())})"
|
319
|
+
)
|