sfq 0.0.8__py3-none-any.whl → 0.0.10__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
@@ -1,3 +1,4 @@
|
|
1
|
+
import base64
|
1
2
|
import http.client
|
2
3
|
import json
|
3
4
|
import logging
|
@@ -70,7 +71,7 @@ class SFAuth:
|
|
70
71
|
access_token: Optional[str] = None,
|
71
72
|
token_expiration_time: Optional[float] = None,
|
72
73
|
token_lifetime: int = 15 * 60,
|
73
|
-
user_agent: str = "sfq/0.0.
|
74
|
+
user_agent: str = "sfq/0.0.10",
|
74
75
|
proxy: str = "auto",
|
75
76
|
) -> None:
|
76
77
|
"""
|
@@ -84,7 +85,7 @@ class SFAuth:
|
|
84
85
|
:param access_token: The access token for the current session (default is None).
|
85
86
|
:param token_expiration_time: The expiration time of the access token (default is None).
|
86
87
|
:param token_lifetime: The lifetime of the access token in seconds (default is 15 minutes).
|
87
|
-
:param user_agent: Custom User-Agent string (default is "sfq/0.0.
|
88
|
+
:param user_agent: Custom User-Agent string (default is "sfq/0.0.10").
|
88
89
|
:param proxy: The proxy configuration, "auto" to use environment (default is "auto").
|
89
90
|
"""
|
90
91
|
self.instance_url = instance_url
|
@@ -130,12 +131,8 @@ class SFAuth:
|
|
130
131
|
"""
|
131
132
|
if self.proxy:
|
132
133
|
proxy_url = urlparse(self.proxy)
|
133
|
-
|
134
|
-
|
135
|
-
if proxy_url.scheme != "http"
|
136
|
-
else http.client.HTTPConnection
|
137
|
-
)
|
138
|
-
conn = conn_cls(proxy_url.hostname, proxy_url.port)
|
134
|
+
logger.trace("Using proxy: %s", self.proxy)
|
135
|
+
conn = http.client.HTTPSConnection(proxy_url.hostname, proxy_url.port)
|
139
136
|
conn.set_tunnel(netloc)
|
140
137
|
logger.trace("Using proxy tunnel to %s", netloc)
|
141
138
|
else:
|
@@ -143,7 +140,7 @@ class SFAuth:
|
|
143
140
|
logger.trace("Direct connection to %s", netloc)
|
144
141
|
return conn
|
145
142
|
|
146
|
-
def
|
143
|
+
def _new_token_request(self, payload: Dict[str, str]) -> Optional[Dict[str, Any]]:
|
147
144
|
"""
|
148
145
|
Send a POST request to the Salesforce token endpoint using http.client.
|
149
146
|
|
@@ -233,7 +230,7 @@ class SFAuth:
|
|
233
230
|
|
234
231
|
logger.trace("Access token expired or missing, refreshing...")
|
235
232
|
payload = self._prepare_payload()
|
236
|
-
token_data = self.
|
233
|
+
token_data = self._new_token_request(payload)
|
237
234
|
|
238
235
|
if token_data:
|
239
236
|
self.access_token = token_data.get("access_token")
|
@@ -243,7 +240,10 @@ class SFAuth:
|
|
243
240
|
self.org_id = token_data.get("id").split("/")[4]
|
244
241
|
self.user_id = token_data.get("id").split("/")[5]
|
245
242
|
logger.trace(
|
246
|
-
"Authenticated as user %s
|
243
|
+
"Authenticated as user %s for org %s (%s)",
|
244
|
+
self.user_id,
|
245
|
+
self.org_id,
|
246
|
+
token_data.get("instance_url"),
|
247
247
|
)
|
248
248
|
except (IndexError, KeyError):
|
249
249
|
logger.error("Failed to extract org/user IDs from token response.")
|
@@ -268,16 +268,192 @@ class SFAuth:
|
|
268
268
|
logger.warning("Token expiration check failed. Treating token as expired.")
|
269
269
|
return True
|
270
270
|
|
271
|
-
def
|
271
|
+
def read_static_resource_name(
|
272
|
+
self, resource_name: str, namespace: Optional[str] = None
|
273
|
+
) -> Optional[str]:
|
272
274
|
"""
|
273
|
-
|
275
|
+
Read a static resource for a given name from the Salesforce instance.
|
274
276
|
|
275
|
-
:param
|
277
|
+
:param resource_name: Name of the static resource to read.
|
278
|
+
:param namespace: Namespace of the static resource to read (default is None).
|
279
|
+
:return: Static resource content or None on failure.
|
280
|
+
"""
|
281
|
+
_safe_resource_name = quote(resource_name, safe="")
|
282
|
+
query = f"SELECT Id FROM StaticResource WHERE Name = '{_safe_resource_name}'"
|
283
|
+
if namespace:
|
284
|
+
query += f" AND NamespacePrefix = '{namespace}'"
|
285
|
+
query += " LIMIT 1"
|
286
|
+
_static_resource_id_response = self.query(query)
|
287
|
+
|
288
|
+
if (
|
289
|
+
_static_resource_id_response
|
290
|
+
and _static_resource_id_response.get("records")
|
291
|
+
and len(_static_resource_id_response["records"]) > 0
|
292
|
+
):
|
293
|
+
return self.read_static_resource_id(
|
294
|
+
_static_resource_id_response["records"][0].get("Id")
|
295
|
+
)
|
296
|
+
|
297
|
+
logger.error(f"Failed to read static resource with name {_safe_resource_name}.")
|
298
|
+
return None
|
299
|
+
|
300
|
+
def read_static_resource_id(self, resource_id: str) -> Optional[str]:
|
301
|
+
"""
|
302
|
+
Read a static resource for a given ID from the Salesforce instance.
|
303
|
+
|
304
|
+
:param resource_id: ID of the static resource to read.
|
305
|
+
:return: Static resource content or None on failure.
|
306
|
+
"""
|
307
|
+
self._refresh_token_if_needed()
|
308
|
+
|
309
|
+
if not self.access_token:
|
310
|
+
logger.error("No access token available for limits.")
|
311
|
+
return None
|
312
|
+
|
313
|
+
endpoint = f"/services/data/{self.api_version}/sobjects/StaticResource/{resource_id}/Body"
|
314
|
+
headers = {
|
315
|
+
"Authorization": f"Bearer {self.access_token}",
|
316
|
+
"User-Agent": self.user_agent,
|
317
|
+
"Accept": "application/json",
|
318
|
+
}
|
319
|
+
|
320
|
+
parsed_url = urlparse(self.instance_url)
|
321
|
+
conn = self._create_connection(parsed_url.netloc)
|
322
|
+
|
323
|
+
try:
|
324
|
+
logger.trace("Request endpoint: %s", endpoint)
|
325
|
+
logger.trace("Request headers: %s", headers)
|
326
|
+
conn.request("GET", endpoint, headers=headers)
|
327
|
+
response = conn.getresponse()
|
328
|
+
data = response.read().decode("utf-8")
|
329
|
+
self._http_resp_header_logic(response)
|
330
|
+
|
331
|
+
if response.status == 200:
|
332
|
+
logger.debug("Get Static Resource Body API request successful.")
|
333
|
+
logger.trace("Response body: %s", data)
|
334
|
+
return data
|
335
|
+
|
336
|
+
logger.error(
|
337
|
+
"Get Static Resource Body API request failed: %s %s",
|
338
|
+
response.status,
|
339
|
+
response.reason,
|
340
|
+
)
|
341
|
+
logger.debug("Response body: %s", data)
|
342
|
+
|
343
|
+
except Exception as err:
|
344
|
+
logger.exception(
|
345
|
+
"Error during Get Static Resource Body API request: %s", err
|
346
|
+
)
|
347
|
+
|
348
|
+
finally:
|
349
|
+
conn.close()
|
350
|
+
|
351
|
+
return None
|
352
|
+
|
353
|
+
def update_static_resource_name(
|
354
|
+
self, resource_name: str, data: str, namespace: Optional[str] = None
|
355
|
+
) -> Optional[Dict[str, Any]]:
|
356
|
+
"""
|
357
|
+
Update a static resource for a given name in the Salesforce instance.
|
358
|
+
|
359
|
+
:param resource_name: Name of the static resource to update.
|
360
|
+
:param data: Content to update the static resource with.
|
361
|
+
:param namespace: Optional namespace to search for the static resource.
|
362
|
+
:return: Static resource content or None on failure.
|
363
|
+
"""
|
364
|
+
safe_resource_name = quote(resource_name, safe="")
|
365
|
+
query = f"SELECT Id FROM StaticResource WHERE Name = '{safe_resource_name}'"
|
366
|
+
if namespace:
|
367
|
+
query += f" AND NamespacePrefix = '{namespace}'"
|
368
|
+
query += " LIMIT 1"
|
369
|
+
|
370
|
+
static_resource_id_response = self.query(query)
|
371
|
+
|
372
|
+
if (
|
373
|
+
static_resource_id_response
|
374
|
+
and static_resource_id_response.get("records")
|
375
|
+
and len(static_resource_id_response["records"]) > 0
|
376
|
+
):
|
377
|
+
return self.update_static_resource_id(
|
378
|
+
static_resource_id_response["records"][0].get("Id"), data
|
379
|
+
)
|
380
|
+
|
381
|
+
logger.error(
|
382
|
+
f"Failed to update static resource with name {safe_resource_name}."
|
383
|
+
)
|
384
|
+
return None
|
385
|
+
|
386
|
+
def update_static_resource_id(
|
387
|
+
self, resource_id: str, data: str
|
388
|
+
) -> Optional[Dict[str, Any]]:
|
389
|
+
"""
|
390
|
+
Replace the content of a static resource in the Salesforce instance by ID.
|
391
|
+
|
392
|
+
:param resource_id: ID of the static resource to update.
|
393
|
+
:param data: Content to update the static resource with.
|
276
394
|
:return: Parsed JSON response or None on failure.
|
277
395
|
"""
|
278
|
-
|
396
|
+
self._refresh_token_if_needed()
|
397
|
+
|
398
|
+
if not self.access_token:
|
399
|
+
logger.error("No access token available for limits.")
|
400
|
+
return None
|
401
|
+
|
402
|
+
payload = {"Body": base64.b64encode(data.encode("utf-8"))}
|
403
|
+
|
404
|
+
endpoint = (
|
405
|
+
f"/services/data/{self.api_version}/sobjects/StaticResource/{resource_id}"
|
406
|
+
)
|
407
|
+
headers = {
|
408
|
+
"Authorization": f"Bearer {self.access_token}",
|
409
|
+
"User-Agent": self.user_agent,
|
410
|
+
"Content-Type": "application/json",
|
411
|
+
"Accept": "application/json",
|
412
|
+
}
|
413
|
+
|
414
|
+
parsed_url = urlparse(self.instance_url)
|
415
|
+
conn = self._create_connection(parsed_url.netloc)
|
416
|
+
|
417
|
+
try:
|
418
|
+
logger.trace("Request endpoint: %s", endpoint)
|
419
|
+
logger.trace("Request headers: %s", headers)
|
420
|
+
logger.trace("Request payload: %s", payload)
|
421
|
+
conn.request(
|
422
|
+
"PATCH",
|
423
|
+
endpoint,
|
424
|
+
headers=headers,
|
425
|
+
body=json.dumps(payload, default=lambda x: x.decode("utf-8")),
|
426
|
+
)
|
427
|
+
response = conn.getresponse()
|
428
|
+
data = response.read().decode("utf-8")
|
429
|
+
self._http_resp_header_logic(response)
|
430
|
+
|
431
|
+
if response.status == 200:
|
432
|
+
logger.debug("Patch Static Resource request successful.")
|
433
|
+
logger.trace("Response body: %s", data)
|
434
|
+
return json.loads(data)
|
435
|
+
|
436
|
+
logger.error(
|
437
|
+
"Patch Static Resource API request failed: %s %s",
|
438
|
+
response.status,
|
439
|
+
response.reason,
|
440
|
+
)
|
441
|
+
logger.debug("Response body: %s", data)
|
442
|
+
|
443
|
+
except Exception as err:
|
444
|
+
logger.exception("Error during patch request: %s", err)
|
445
|
+
|
446
|
+
finally:
|
447
|
+
conn.close()
|
448
|
+
|
449
|
+
return None
|
279
450
|
|
280
451
|
def limits(self) -> Optional[Dict[str, Any]]:
|
452
|
+
"""
|
453
|
+
Execute a GET request to the Salesforce Limits API.
|
454
|
+
|
455
|
+
:return: Parsed JSON response or None on failure.
|
456
|
+
"""
|
281
457
|
self._refresh_token_if_needed()
|
282
458
|
|
283
459
|
if not self.access_token:
|
@@ -288,7 +464,6 @@ class SFAuth:
|
|
288
464
|
headers = {
|
289
465
|
"Authorization": f"Bearer {self.access_token}",
|
290
466
|
"User-Agent": self.user_agent,
|
291
|
-
"Content-Type": "application/x-www-form-urlencoded",
|
292
467
|
"Accept": "application/json",
|
293
468
|
}
|
294
469
|
|
@@ -308,7 +483,9 @@ class SFAuth:
|
|
308
483
|
logger.trace("Response body: %s", data)
|
309
484
|
return json.loads(data)
|
310
485
|
|
311
|
-
logger.error(
|
486
|
+
logger.error(
|
487
|
+
"Limits API request failed: %s %s", response.status, response.reason
|
488
|
+
)
|
312
489
|
logger.debug("Response body: %s", data)
|
313
490
|
|
314
491
|
except Exception as err:
|
@@ -342,7 +519,6 @@ class SFAuth:
|
|
342
519
|
headers = {
|
343
520
|
"Authorization": f"Bearer {self.access_token}",
|
344
521
|
"User-Agent": self.user_agent,
|
345
|
-
"Content-Type": "application/x-www-form-urlencoded",
|
346
522
|
"Accept": "application/json",
|
347
523
|
}
|
348
524
|
|
@@ -400,3 +576,12 @@ class SFAuth:
|
|
400
576
|
conn.close()
|
401
577
|
|
402
578
|
return None
|
579
|
+
|
580
|
+
def tooling_query(self, query: str) -> Optional[Dict[str, Any]]:
|
581
|
+
"""
|
582
|
+
Execute a SOQL query using the Tooling API.
|
583
|
+
|
584
|
+
:param query: The SOQL query string.
|
585
|
+
:return: Parsed JSON response or None on failure.
|
586
|
+
"""
|
587
|
+
return self.query(query, tooling=True)
|
@@ -0,0 +1,5 @@
|
|
1
|
+
sfq/__init__.py,sha256=oUFxQHH31dAkke5OX7IxVaiXSmxbNdHXM6NasVAvB8I,21809
|
2
|
+
sfq/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
3
|
+
sfq-0.0.10.dist-info/METADATA,sha256=9Gm_aVMOPJkR0Kj1ZHLMprgzAq-PAng2OALYku1eNWA,5067
|
4
|
+
sfq-0.0.10.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
5
|
+
sfq-0.0.10.dist-info/RECORD,,
|
sfq-0.0.8.dist-info/RECORD
DELETED
@@ -1,5 +0,0 @@
|
|
1
|
-
sfq/__init__.py,sha256=MlnlaJCHz4NgL_nudfa15Aman7LdgEtv-hY3G-M731E,15203
|
2
|
-
sfq/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
3
|
-
sfq-0.0.8.dist-info/METADATA,sha256=I1iwzWgydgwKKslNoZvc5y0VjPKytm1R5B1wLXm7VoY,5066
|
4
|
-
sfq-0.0.8.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
5
|
-
sfq-0.0.8.dist-info/RECORD,,
|
File without changes
|