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.
|
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.
|
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.
|
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
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,14 +1,14 @@
|
|
1
|
-
sfq/__init__.py,sha256=
|
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
|
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=
|
12
|
-
sfq-0.0.
|
13
|
-
sfq-0.0.
|
14
|
-
sfq-0.0.
|
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
|