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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sfq
3
- Version: 0.0.9
3
+ Version: 0.0.10
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.9"
3
+ version = "0.0.10"
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,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.9",
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.9").
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 _post_token_request(self, payload: Dict[str, str]) -> Optional[Dict[str, Any]]:
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._post_token_request(payload)
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 in org %s", self.user_id, self.org_id
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 tooling_query(self, query: str) -> Optional[Dict[str, Any]]:
271
+ def read_static_resource_name(
272
+ self, resource_name: str, namespace: Optional[str] = None
273
+ ) -> Optional[str]:
268
274
  """
269
- Execute a SOQL query using the Tooling API.
275
+ Read a static resource for a given name from the Salesforce instance.
270
276
 
271
- :param query: The SOQL query string.
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
- return self.query(query, tooling=True)
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("Limits API request failed: %s %s", response.status, response.reason)
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)
@@ -3,5 +3,5 @@ requires-python = ">=3.9"
3
3
 
4
4
  [[package]]
5
5
  name = "sfq"
6
- version = "0.0.9"
6
+ version = "0.0.10"
7
7
  source = { editable = "." }
File without changes
File without changes
File without changes
File without changes
File without changes