sfq 0.0.9__tar.gz → 0.0.10__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.9 → sfq-0.0.10}/PKG-INFO +1 -1
- {sfq-0.0.9 → sfq-0.0.10}/pyproject.toml +1 -1
- {sfq-0.0.9 → sfq-0.0.10}/src/sfq/__init__.py +201 -12
- {sfq-0.0.9 → sfq-0.0.10}/uv.lock +1 -1
- {sfq-0.0.9 → sfq-0.0.10}/.github/workflows/publish.yml +0 -0
- {sfq-0.0.9 → sfq-0.0.10}/.gitignore +0 -0
- {sfq-0.0.9 → sfq-0.0.10}/.python-version +0 -0
- {sfq-0.0.9 → sfq-0.0.10}/README.md +0 -0
- {sfq-0.0.9 → sfq-0.0.10}/src/sfq/py.typed +0 -0
@@ -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
|
@@ -139,7 +140,7 @@ class SFAuth:
|
|
139
140
|
logger.trace("Direct connection to %s", netloc)
|
140
141
|
return conn
|
141
142
|
|
142
|
-
def
|
143
|
+
def _new_token_request(self, payload: Dict[str, str]) -> Optional[Dict[str, Any]]:
|
143
144
|
"""
|
144
145
|
Send a POST request to the Salesforce token endpoint using http.client.
|
145
146
|
|
@@ -229,7 +230,7 @@ class SFAuth:
|
|
229
230
|
|
230
231
|
logger.trace("Access token expired or missing, refreshing...")
|
231
232
|
payload = self._prepare_payload()
|
232
|
-
token_data = self.
|
233
|
+
token_data = self._new_token_request(payload)
|
233
234
|
|
234
235
|
if token_data:
|
235
236
|
self.access_token = token_data.get("access_token")
|
@@ -239,7 +240,10 @@ class SFAuth:
|
|
239
240
|
self.org_id = token_data.get("id").split("/")[4]
|
240
241
|
self.user_id = token_data.get("id").split("/")[5]
|
241
242
|
logger.trace(
|
242
|
-
"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"),
|
243
247
|
)
|
244
248
|
except (IndexError, KeyError):
|
245
249
|
logger.error("Failed to extract org/user IDs from token response.")
|
@@ -264,16 +268,192 @@ class SFAuth:
|
|
264
268
|
logger.warning("Token expiration check failed. Treating token as expired.")
|
265
269
|
return True
|
266
270
|
|
267
|
-
def
|
271
|
+
def read_static_resource_name(
|
272
|
+
self, resource_name: str, namespace: Optional[str] = None
|
273
|
+
) -> Optional[str]:
|
268
274
|
"""
|
269
|
-
|
275
|
+
Read a static resource for a given name from the Salesforce instance.
|
270
276
|
|
271
|
-
: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.
|
272
394
|
:return: Parsed JSON response or None on failure.
|
273
395
|
"""
|
274
|
-
|
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
|
275
450
|
|
276
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
|
+
"""
|
277
457
|
self._refresh_token_if_needed()
|
278
458
|
|
279
459
|
if not self.access_token:
|
@@ -284,7 +464,6 @@ class SFAuth:
|
|
284
464
|
headers = {
|
285
465
|
"Authorization": f"Bearer {self.access_token}",
|
286
466
|
"User-Agent": self.user_agent,
|
287
|
-
"Content-Type": "application/x-www-form-urlencoded",
|
288
467
|
"Accept": "application/json",
|
289
468
|
}
|
290
469
|
|
@@ -304,7 +483,9 @@ class SFAuth:
|
|
304
483
|
logger.trace("Response body: %s", data)
|
305
484
|
return json.loads(data)
|
306
485
|
|
307
|
-
logger.error(
|
486
|
+
logger.error(
|
487
|
+
"Limits API request failed: %s %s", response.status, response.reason
|
488
|
+
)
|
308
489
|
logger.debug("Response body: %s", data)
|
309
490
|
|
310
491
|
except Exception as err:
|
@@ -338,7 +519,6 @@ class SFAuth:
|
|
338
519
|
headers = {
|
339
520
|
"Authorization": f"Bearer {self.access_token}",
|
340
521
|
"User-Agent": self.user_agent,
|
341
|
-
"Content-Type": "application/x-www-form-urlencoded",
|
342
522
|
"Accept": "application/json",
|
343
523
|
}
|
344
524
|
|
@@ -396,3 +576,12 @@ class SFAuth:
|
|
396
576
|
conn.close()
|
397
577
|
|
398
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)
|
{sfq-0.0.9 → sfq-0.0.10}/uv.lock
RENAMED
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|