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.8",
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.8").
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
- conn_cls = (
134
- http.client.HTTPSConnection
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 _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]]:
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._post_token_request(payload)
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 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"),
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 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]:
272
274
  """
273
- Execute a SOQL query using the Tooling API.
275
+ Read a static resource for a given name from the Salesforce instance.
274
276
 
275
- :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.
276
394
  :return: Parsed JSON response or None on failure.
277
395
  """
278
- 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
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("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
+ )
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)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sfq
3
- Version: 0.0.8
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
@@ -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,,
@@ -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