sfq 0.0.35__py3-none-any.whl → 0.0.36__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
@@ -24,7 +24,7 @@ from .exceptions import (
24
24
  from .http_client import HTTPClient
25
25
  from .query import QueryClient
26
26
  from .soap import SOAPClient
27
- from .utils import get_logger
27
+ from .utils import get_logger, records_to_html_table
28
28
  from .debug_cleanup import DebugCleanup
29
29
 
30
30
  # Define public API for documentation tools
@@ -43,7 +43,7 @@ __all__ = [
43
43
  "__version__",
44
44
  ]
45
45
 
46
- __version__ = "0.0.35"
46
+ __version__ = "0.0.36"
47
47
  """
48
48
  ### `__version__`
49
49
 
@@ -67,7 +67,7 @@ class SFAuth:
67
67
  access_token: Optional[str] = None,
68
68
  token_expiration_time: Optional[float] = None,
69
69
  token_lifetime: int = 15 * 60,
70
- user_agent: str = "sfq/0.0.35",
70
+ user_agent: str = "sfq/0.0.36",
71
71
  sforce_client: str = "_auto",
72
72
  proxy: str = "_auto",
73
73
  ) -> None:
@@ -132,7 +132,7 @@ class SFAuth:
132
132
  self._debug_cleanup = DebugCleanup(sf_auth=self)
133
133
 
134
134
  # Store version information
135
- self.__version__ = "0.0.35"
135
+ self.__version__ = "0.0.36"
136
136
  """
137
137
  ### `__version__`
138
138
 
@@ -561,3 +561,19 @@ class SFAuth:
561
561
  sid = quote(self.access_token, safe="")
562
562
  frontdoor_url = f"{self.instance_url}/secur/frontdoor.jsp?sid={sid}"
563
563
  webbrowser.open(frontdoor_url)
564
+
565
+ def records_to_html_table(
566
+ self,
567
+ items: List[Dict[str, Any]],
568
+ styled: bool = False,
569
+ ) -> str:
570
+ """
571
+ Convert a list of dictionaries to an HTML table.
572
+
573
+ :param items: List of dictionaries to convert.
574
+ :param styled: If True, apply basic CSS styles to the table.
575
+ :return: HTML string representing the table.
576
+ """
577
+ if "records" in items:
578
+ items = items["records"]
579
+ return records_to_html_table(items, styled=styled)
sfq/http_client.py CHANGED
@@ -28,7 +28,7 @@ class HTTPClient:
28
28
  def __init__(
29
29
  self,
30
30
  auth_manager: AuthManager,
31
- user_agent: str = "sfq/0.0.35",
31
+ user_agent: str = "sfq/0.0.36",
32
32
  sforce_client: str = "_auto",
33
33
  high_api_usage_threshold: int = 80,
34
34
  ) -> None:
sfq/utils.py CHANGED
@@ -9,6 +9,7 @@ sensitive data redaction functionality.
9
9
  import json
10
10
  import logging
11
11
  import re
12
+ from html import escape
12
13
  from typing import Any, Dict, List, Tuple, Union
13
14
 
14
15
  # Custom TRACE logging level
@@ -194,3 +195,127 @@ def extract_org_and_user_ids(token_id_url: str) -> Tuple[str, str]:
194
195
  return org_id, user_id
195
196
  except (IndexError, AttributeError):
196
197
  raise ValueError(f"Invalid token ID URL format: {token_id_url}")
198
+
199
+
200
+ def dicts_to_html_table(
201
+ items: List[Dict[str, Any]], styled: bool = False
202
+ ) -> str:
203
+ """
204
+ Convert a list of dictionaries to a compact HTML table.
205
+
206
+ :param items: List of dictionaries to convert
207
+ :param styled: If True, apply minimal inline CSS for compact styling
208
+ :return: HTML string for a table with one column per key.
209
+ :raises ValueError: If input is not a list of dictionaries, or if keys are invalid types.
210
+ """
211
+ if not isinstance(items, list):
212
+ raise ValueError("Input must be a list of dictionaries.")
213
+
214
+ def render_value(val: Any) -> str:
215
+ if val is None:
216
+ return ""
217
+ if isinstance(val, (int, float, str, bool)):
218
+ return escape(str(val))
219
+ if isinstance(val, list):
220
+ return (
221
+ "<ul>"
222
+ + "".join(f"<li>{render_value(item)}</li>" for item in val)
223
+ + "</ul>"
224
+ )
225
+ if isinstance(val, dict):
226
+ return dicts_to_html_table([val], styled=styled)
227
+ try:
228
+ dumped = json.dumps(val, default=str)
229
+ if dumped.startswith('"') and dumped.endswith('"'):
230
+ dumped = dumped[1:-1]
231
+ return escape(dumped)
232
+ except Exception:
233
+ return escape(str(val))
234
+
235
+ # Preserve key order by first appearance
236
+ columns: List[str] = []
237
+ seen = set()
238
+ for i, d in enumerate(items):
239
+ if not isinstance(d, dict):
240
+ raise ValueError(f"Element at index {i} is not a dictionary.")
241
+ for k in d.keys():
242
+ k_str = (
243
+ k
244
+ if isinstance(k, str)
245
+ else str(k)
246
+ if isinstance(k, (int, float, bool))
247
+ else None
248
+ )
249
+ if k_str is None:
250
+ raise ValueError(f"Dictionary at index {i} has a non-string key: {k!r}")
251
+ if k_str not in seen:
252
+ columns.append(k_str)
253
+ seen.add(k_str)
254
+
255
+ # Build table
256
+ def get_value_by_str_key(d: Dict[Any, Any], col: str) -> Any:
257
+ for k in d.keys():
258
+ k_str = (
259
+ k
260
+ if isinstance(k, str)
261
+ else str(k)
262
+ if isinstance(k, (int, float, bool))
263
+ else None
264
+ )
265
+ if k_str == col:
266
+ return d[k]
267
+ return ""
268
+
269
+ if styled:
270
+ table_style = (
271
+ "border-collapse:collapse;font-size:12px;line-height:1.2;"
272
+ "margin:0;padding:0;width:auto;"
273
+ )
274
+ td_style = "border:1px solid #ccc;padding:2px 6px;vertical-align:top;"
275
+ th_style = (
276
+ "border:1px solid #ccc;padding:2px 6px;background:#f8f8f8;font-weight:bold;"
277
+ )
278
+ if columns:
279
+ html = [f'<table style="{table_style}"><thead><tr>']
280
+ html.extend(f'<th style="{th_style}">{escape(col)}</th>' for col in columns)
281
+ html.append("</tr></thead><tbody>")
282
+ for d in items:
283
+ html.append("<tr>")
284
+ html.extend(
285
+ f'<td style="{td_style}">{render_value(get_value_by_str_key(d, col))}</td>'
286
+ for col in columns
287
+ )
288
+ html.append("</tr>")
289
+ html.append("</tbody></table>")
290
+ else:
291
+ html = [
292
+ f'<table style="{table_style}"><thead></thead><tbody></tbody></table>'
293
+ ]
294
+ else:
295
+ if columns:
296
+ html = ["<table><thead><tr>"]
297
+ html.extend(f"<th>{escape(col)}</th>" for col in columns)
298
+ html.append("</tr></thead><tbody>")
299
+ for d in items:
300
+ html.append("<tr>")
301
+ html.extend(
302
+ f"<td>{render_value(get_value_by_str_key(d, col))}</td>"
303
+ for col in columns
304
+ )
305
+ html.append("</tr>")
306
+ html.append("</tbody></table>")
307
+ else:
308
+ html = ["<table><thead></thead><tbody></tbody></table>"]
309
+
310
+ return "".join(html)
311
+
312
+ def records_to_html_table(records: List[Dict[str, Any]], styled: bool = False) -> str:
313
+ """Convert a list of records to an HTML table."""
314
+ # really we don't want anything associated with "attributes"
315
+ normalized_records = []
316
+ for record in records:
317
+ if not isinstance(record, dict):
318
+ raise ValueError(f"Record is not a dictionary: {record!r}")
319
+ record.pop("attributes", None)
320
+ normalized_records.append(record)
321
+ return dicts_to_html_table(normalized_records, styled=styled)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sfq
3
- Version: 0.0.35
3
+ Version: 0.0.36
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,14 +1,14 @@
1
- sfq/__init__.py,sha256=dfNoQFuitLT_j-nKPtsqYpeJLxwIzwBgPjiuVerelfk,19929
1
+ sfq/__init__.py,sha256=VlPphhQ59mVW-BmJHE_S5P4-v1czO-CSX9icraMUx3E,20465
2
2
  sfq/_cometd.py,sha256=QqdSGsms9uFm7vgmxgau7m2UuLHztK1yjN-BNjeo8xM,10381
3
3
  sfq/auth.py,sha256=bD7kEI5UpUAh0xpE2GzB7EatfLE0q-rqG7tOpqn_cQY,13985
4
4
  sfq/crud.py,sha256=fj4wPMt0DcrMKbMWQ9AUMsUNUWicsY93LP_3Q7lhmDU,20300
5
5
  sfq/debug_cleanup.py,sha256=e2_Hpigy3F7XsATOUXo8DZNmuEIL9SDD0tBlZIZeQLc,2638
6
6
  sfq/exceptions.py,sha256=HZctvGj1SGguca0oG6fqSmf3KDbq4v68FfQfqB-crpo,906
7
- sfq/http_client.py,sha256=-Xs3I9jW-3l098wdi7CMnS4wWwffZ8EeQ_xygacaNHg,11636
7
+ sfq/http_client.py,sha256=7SWfnJxHZMNphPc-pd3DUzPDrJymPQ9znNMSALnFiHQ,11636
8
8
  sfq/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
9
9
  sfq/query.py,sha256=AoagL8PMKUcpbPPTcHJPKhmUdDDPa0La4JLC0TUN_Yc,14586
10
10
  sfq/soap.py,sha256=FM4msP9ErrgLFaNOQy_kYVde8QFkT4yQu9TfMiZG0VA,7006
11
- sfq/utils.py,sha256=gx_pCZmOykYz19wwx6O2BTGj7bQfzhX_SuLHnYGCWuc,6234
12
- sfq-0.0.35.dist-info/METADATA,sha256=mXHjqsfIWDzGKnFe7dzZVEj54yqT4xdd1702heA4Vh8,6899
13
- sfq-0.0.35.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
14
- sfq-0.0.35.dist-info/RECORD,,
11
+ sfq/utils.py,sha256=3RjloIAO0A2i8nkbPwB6m9nnKhMvXBSsqnnKFmY1cwY,10782
12
+ sfq-0.0.36.dist-info/METADATA,sha256=_6EWxpLVyuecOv0St_dTygTIt8wXodqtgx3cfQnNKHc,6899
13
+ sfq-0.0.36.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
14
+ sfq-0.0.36.dist-info/RECORD,,
File without changes