sfq 0.0.35__tar.gz → 0.0.37__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.35 → sfq-0.0.37}/.github/workflows/publish.yml +1 -0
- {sfq-0.0.35 → sfq-0.0.37}/PKG-INFO +1 -1
- {sfq-0.0.35 → sfq-0.0.37}/pyproject.toml +1 -1
- {sfq-0.0.35 → sfq-0.0.37}/src/sfq/__init__.py +20 -4
- {sfq-0.0.35 → sfq-0.0.37}/src/sfq/http_client.py +1 -1
- {sfq-0.0.35 → sfq-0.0.37}/src/sfq/utils.py +125 -0
- sfq-0.0.37/tests/html/test_complex_nested.html +1 -0
- sfq-0.0.37/tests/html/test_complex_nested_styled.html +1 -0
- sfq-0.0.37/tests/html/test_empty_list.html +1 -0
- sfq-0.0.37/tests/html/test_empty_list_styled.html +1 -0
- sfq-0.0.37/tests/html/test_int_float_bool.html +1 -0
- sfq-0.0.37/tests/html/test_int_float_bool_styled.html +1 -0
- sfq-0.0.37/tests/html/test_list_value.html +1 -0
- sfq-0.0.37/tests/html/test_list_value_styled.html +1 -0
- sfq-0.0.37/tests/html/test_multiple_dicts.html +1 -0
- sfq-0.0.37/tests/html/test_multiple_dicts_styled.html +1 -0
- sfq-0.0.37/tests/html/test_nested_dict.html +1 -0
- sfq-0.0.37/tests/html/test_nested_dict_styled.html +1 -0
- sfq-0.0.37/tests/html/test_none_value.html +1 -0
- sfq-0.0.37/tests/html/test_none_value_styled.html +1 -0
- sfq-0.0.37/tests/html/test_other_types.html +1 -0
- sfq-0.0.37/tests/html/test_other_types_styled.html +1 -0
- sfq-0.0.37/tests/html/test_sample_report.html +1 -0
- sfq-0.0.37/tests/html/test_sample_report_styled.html +1 -0
- sfq-0.0.37/tests/html/test_single_flat_dict.html +1 -0
- sfq-0.0.37/tests/html/test_single_flat_dict_styled.html +1 -0
- sfq-0.0.37/tests/html/test_typecastable_keys_bool.html +1 -0
- sfq-0.0.37/tests/html/test_typecastable_keys_bool_styled.html +1 -0
- sfq-0.0.37/tests/html/test_typecastable_keys_float.html +1 -0
- sfq-0.0.37/tests/html/test_typecastable_keys_float_styled.html +1 -0
- sfq-0.0.37/tests/html/test_typecastable_keys_int.html +1 -0
- sfq-0.0.37/tests/html/test_typecastable_keys_int_styled.html +1 -0
- sfq-0.0.37/tests/test_records_to_html.py +45 -0
- sfq-0.0.37/tests/test_utils_html_table.py +190 -0
- {sfq-0.0.35 → sfq-0.0.37}/uv.lock +1 -1
- {sfq-0.0.35 → sfq-0.0.37}/.gitignore +0 -0
- {sfq-0.0.35 → sfq-0.0.37}/.python-version +0 -0
- {sfq-0.0.35 → sfq-0.0.37}/README.md +0 -0
- {sfq-0.0.35 → sfq-0.0.37}/src/sfq/_cometd.py +0 -0
- {sfq-0.0.35 → sfq-0.0.37}/src/sfq/auth.py +0 -0
- {sfq-0.0.35 → sfq-0.0.37}/src/sfq/crud.py +0 -0
- {sfq-0.0.35 → sfq-0.0.37}/src/sfq/debug_cleanup.py +0 -0
- {sfq-0.0.35 → sfq-0.0.37}/src/sfq/exceptions.py +0 -0
- {sfq-0.0.35 → sfq-0.0.37}/src/sfq/py.typed +0 -0
- {sfq-0.0.35 → sfq-0.0.37}/src/sfq/query.py +0 -0
- {sfq-0.0.35 → sfq-0.0.37}/src/sfq/soap.py +0 -0
- {sfq-0.0.35 → sfq-0.0.37}/tests/test_auth.py +0 -0
- {sfq-0.0.35 → sfq-0.0.37}/tests/test_cdelete.py +0 -0
- {sfq-0.0.35 → sfq-0.0.37}/tests/test_compatibility.py +0 -0
- {sfq-0.0.35 → sfq-0.0.37}/tests/test_cquery.py +0 -0
- {sfq-0.0.35 → sfq-0.0.37}/tests/test_create.py +0 -0
- {sfq-0.0.35 → sfq-0.0.37}/tests/test_crud.py +0 -0
- {sfq-0.0.35 → sfq-0.0.37}/tests/test_crud_e2e.py +0 -0
- {sfq-0.0.35 → sfq-0.0.37}/tests/test_cupdate.py +0 -0
- {sfq-0.0.35 → sfq-0.0.37}/tests/test_debug_cleanup_e2e.py +0 -0
- {sfq-0.0.35 → sfq-0.0.37}/tests/test_debug_cleanup_unit.py +0 -0
- {sfq-0.0.35 → sfq-0.0.37}/tests/test_http_client.py +0 -0
- {sfq-0.0.35 → sfq-0.0.37}/tests/test_limits_api.py +0 -0
- {sfq-0.0.35 → sfq-0.0.37}/tests/test_log_trace_redact.py +0 -0
- {sfq-0.0.35 → sfq-0.0.37}/tests/test_open_frontdoor.py +0 -0
- {sfq-0.0.35 → sfq-0.0.37}/tests/test_query.py +0 -0
- {sfq-0.0.35 → sfq-0.0.37}/tests/test_query_client.py +0 -0
- {sfq-0.0.35 → sfq-0.0.37}/tests/test_query_e2e.py +0 -0
- {sfq-0.0.35 → sfq-0.0.37}/tests/test_query_integration.py +0 -0
- {sfq-0.0.35 → sfq-0.0.37}/tests/test_soap.py +0 -0
- {sfq-0.0.35 → sfq-0.0.37}/tests/test_soap_batch_operation.py +0 -0
- {sfq-0.0.35 → sfq-0.0.37}/tests/test_static_resources.py +0 -0
- {sfq-0.0.35 → sfq-0.0.37}/tests/test_utils.py +0 -0
@@ -85,6 +85,7 @@ jobs:
|
|
85
85
|
|
86
86
|
echo "Updating src/sfq/http_client.py user_agent to $VERSION"
|
87
87
|
sed -i -E "s/(user_agent: str = \"sfq\/)[0-9]+\.[0-9]+\.[0-9]+(\")/\1$VERSION\2/" src/sfq/http_client.py
|
88
|
+
sed -i -E "s/(user_agent: str = \"sfq\/)[0-9]+\.[0-9]+\.[0-9]+(\",)/\1$VERSION\2/" src/sfq/http_client.py
|
88
89
|
|
89
90
|
- name: Run tests
|
90
91
|
run: pytest --verbose --strict-config
|
@@ -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.37"
|
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.37",
|
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.37"
|
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)
|
@@ -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 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}">{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>{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)
|
@@ -0,0 +1 @@
|
|
1
|
+
<table><thead><tr><th>a</th></tr></thead><tbody><tr><td><ul><li>1</li><li><table><thead><tr><th>b</th></tr></thead><tbody><tr><td><ul><li>2</li><li>3</li></ul></td></tr></tbody></table></li><li></li></ul></td></tr></tbody></table>
|
@@ -0,0 +1 @@
|
|
1
|
+
<table style="border-collapse:collapse;font-size:12px;line-height:1.2;margin:0;padding:0;width:auto;"><thead><tr><th style="border:1px solid #ccc;padding:2px 6px;background:#f8f8f8;font-weight:bold;">a</th></tr></thead><tbody><tr><td style="border:1px solid #ccc;padding:2px 6px;vertical-align:top;"><ul><li>1</li><li><table style="border-collapse:collapse;font-size:12px;line-height:1.2;margin:0;padding:0;width:auto;"><thead><tr><th style="border:1px solid #ccc;padding:2px 6px;background:#f8f8f8;font-weight:bold;">b</th></tr></thead><tbody><tr><td style="border:1px solid #ccc;padding:2px 6px;vertical-align:top;"><ul><li>2</li><li>3</li></ul></td></tr></tbody></table></li><li></li></ul></td></tr></tbody></table>
|
@@ -0,0 +1 @@
|
|
1
|
+
<table><thead></thead><tbody></tbody></table>
|
@@ -0,0 +1 @@
|
|
1
|
+
<table style="border-collapse:collapse;font-size:12px;line-height:1.2;margin:0;padding:0;width:auto;"><thead></thead><tbody></tbody></table>
|
@@ -0,0 +1 @@
|
|
1
|
+
<table><thead><tr><th>i</th><th>f</th><th>b</th></tr></thead><tbody><tr><td>1</td><td>2.5</td><td>True</td></tr></tbody></table>
|
@@ -0,0 +1 @@
|
|
1
|
+
<table style="border-collapse:collapse;font-size:12px;line-height:1.2;margin:0;padding:0;width:auto;"><thead><tr><th style="border:1px solid #ccc;padding:2px 6px;background:#f8f8f8;font-weight:bold;">i</th><th style="border:1px solid #ccc;padding:2px 6px;background:#f8f8f8;font-weight:bold;">f</th><th style="border:1px solid #ccc;padding:2px 6px;background:#f8f8f8;font-weight:bold;">b</th></tr></thead><tbody><tr><td style="border:1px solid #ccc;padding:2px 6px;vertical-align:top;">1</td><td style="border:1px solid #ccc;padding:2px 6px;vertical-align:top;">2.5</td><td style="border:1px solid #ccc;padding:2px 6px;vertical-align:top;">True</td></tr></tbody></table>
|
@@ -0,0 +1 @@
|
|
1
|
+
<table><thead><tr><th>hello</th></tr></thead><tbody><tr><td><ul><li>bob</li><li>sally</li></ul></td></tr></tbody></table>
|
@@ -0,0 +1 @@
|
|
1
|
+
<table style="border-collapse:collapse;font-size:12px;line-height:1.2;margin:0;padding:0;width:auto;"><thead><tr><th style="border:1px solid #ccc;padding:2px 6px;background:#f8f8f8;font-weight:bold;">hello</th></tr></thead><tbody><tr><td style="border:1px solid #ccc;padding:2px 6px;vertical-align:top;"><ul><li>bob</li><li>sally</li></ul></td></tr></tbody></table>
|
@@ -0,0 +1 @@
|
|
1
|
+
<table><thead><tr><th>x</th><th>z</th></tr></thead><tbody><tr><td>y</td><td></td></tr><tr><td></td><td>w</td></tr></tbody></table>
|
@@ -0,0 +1 @@
|
|
1
|
+
<table style="border-collapse:collapse;font-size:12px;line-height:1.2;margin:0;padding:0;width:auto;"><thead><tr><th style="border:1px solid #ccc;padding:2px 6px;background:#f8f8f8;font-weight:bold;">x</th><th style="border:1px solid #ccc;padding:2px 6px;background:#f8f8f8;font-weight:bold;">z</th></tr></thead><tbody><tr><td style="border:1px solid #ccc;padding:2px 6px;vertical-align:top;">y</td><td style="border:1px solid #ccc;padding:2px 6px;vertical-align:top;"></td></tr><tr><td style="border:1px solid #ccc;padding:2px 6px;vertical-align:top;"></td><td style="border:1px solid #ccc;padding:2px 6px;vertical-align:top;">w</td></tr></tbody></table>
|
@@ -0,0 +1 @@
|
|
1
|
+
<table><thead><tr><th>outer</th></tr></thead><tbody><tr><td><table><thead><tr><th>inner</th></tr></thead><tbody><tr><td>value</td></tr></tbody></table></td></tr></tbody></table>
|
@@ -0,0 +1 @@
|
|
1
|
+
<table style="border-collapse:collapse;font-size:12px;line-height:1.2;margin:0;padding:0;width:auto;"><thead><tr><th style="border:1px solid #ccc;padding:2px 6px;background:#f8f8f8;font-weight:bold;">outer</th></tr></thead><tbody><tr><td style="border:1px solid #ccc;padding:2px 6px;vertical-align:top;"><table style="border-collapse:collapse;font-size:12px;line-height:1.2;margin:0;padding:0;width:auto;"><thead><tr><th style="border:1px solid #ccc;padding:2px 6px;background:#f8f8f8;font-weight:bold;">inner</th></tr></thead><tbody><tr><td style="border:1px solid #ccc;padding:2px 6px;vertical-align:top;">value</td></tr></tbody></table></td></tr></tbody></table>
|
@@ -0,0 +1 @@
|
|
1
|
+
<table><thead><tr><th>foo</th></tr></thead><tbody><tr><td></td></tr></tbody></table>
|
@@ -0,0 +1 @@
|
|
1
|
+
<table style="border-collapse:collapse;font-size:12px;line-height:1.2;margin:0;padding:0;width:auto;"><thead><tr><th style="border:1px solid #ccc;padding:2px 6px;background:#f8f8f8;font-weight:bold;">foo</th></tr></thead><tbody><tr><td style="border:1px solid #ccc;padding:2px 6px;vertical-align:top;"></td></tr></tbody></table>
|
@@ -0,0 +1 @@
|
|
1
|
+
<table><thead><tr><th>custom</th></tr></thead><tbody><tr><td>custom_str</td></tr></tbody></table>
|
@@ -0,0 +1 @@
|
|
1
|
+
<table style="border-collapse:collapse;font-size:12px;line-height:1.2;margin:0;padding:0;width:auto;"><thead><tr><th style="border:1px solid #ccc;padding:2px 6px;background:#f8f8f8;font-weight:bold;">custom</th></tr></thead><tbody><tr><td style="border:1px solid #ccc;padding:2px 6px;vertical-align:top;">custom_str</td></tr></tbody></table>
|
@@ -0,0 +1 @@
|
|
1
|
+
<table><thead><tr><th>name</th><th>date</th><th>status</th></tr></thead><tbody><tr><td>Sample Report</td><td>2023-01-01</td><td>Completed</td></tr><tr><td>Sample Report 2</td><td>2023-01-02</td><td>In Progress</td></tr><tr><td>Sample Report 3</td><td>2023-01-03</td><td>Not Started</td></tr></tbody></table>
|
@@ -0,0 +1 @@
|
|
1
|
+
<table style="border-collapse:collapse;font-size:12px;line-height:1.2;margin:0;padding:0;width:auto;"><thead><tr><th style="border:1px solid #ccc;padding:2px 6px;background:#f8f8f8;font-weight:bold;">name</th><th style="border:1px solid #ccc;padding:2px 6px;background:#f8f8f8;font-weight:bold;">date</th><th style="border:1px solid #ccc;padding:2px 6px;background:#f8f8f8;font-weight:bold;">status</th></tr></thead><tbody><tr><td style="border:1px solid #ccc;padding:2px 6px;vertical-align:top;">Sample Report</td><td style="border:1px solid #ccc;padding:2px 6px;vertical-align:top;">2023-01-01</td><td style="border:1px solid #ccc;padding:2px 6px;vertical-align:top;">Completed</td></tr><tr><td style="border:1px solid #ccc;padding:2px 6px;vertical-align:top;">Sample Report 2</td><td style="border:1px solid #ccc;padding:2px 6px;vertical-align:top;">2023-01-02</td><td style="border:1px solid #ccc;padding:2px 6px;vertical-align:top;">In Progress</td></tr><tr><td style="border:1px solid #ccc;padding:2px 6px;vertical-align:top;">Sample Report 3</td><td style="border:1px solid #ccc;padding:2px 6px;vertical-align:top;">2023-01-03</td><td style="border:1px solid #ccc;padding:2px 6px;vertical-align:top;">Not Started</td></tr></tbody></table>
|
@@ -0,0 +1 @@
|
|
1
|
+
<table><thead><tr><th>a</th><th>c</th></tr></thead><tbody><tr><td>b</td><td>d</td></tr></tbody></table>
|
@@ -0,0 +1 @@
|
|
1
|
+
<table style="border-collapse:collapse;font-size:12px;line-height:1.2;margin:0;padding:0;width:auto;"><thead><tr><th style="border:1px solid #ccc;padding:2px 6px;background:#f8f8f8;font-weight:bold;">a</th><th style="border:1px solid #ccc;padding:2px 6px;background:#f8f8f8;font-weight:bold;">c</th></tr></thead><tbody><tr><td style="border:1px solid #ccc;padding:2px 6px;vertical-align:top;">b</td><td style="border:1px solid #ccc;padding:2px 6px;vertical-align:top;">d</td></tr></tbody></table>
|
@@ -0,0 +1 @@
|
|
1
|
+
<table><thead><tr><th>True</th><th>False</th></tr></thead><tbody><tr><td>truthy</td><td></td></tr><tr><td></td><td>falsey</td></tr></tbody></table>
|
@@ -0,0 +1 @@
|
|
1
|
+
<table style="border-collapse:collapse;font-size:12px;line-height:1.2;margin:0;padding:0;width:auto;"><thead><tr><th style="border:1px solid #ccc;padding:2px 6px;background:#f8f8f8;font-weight:bold;">True</th><th style="border:1px solid #ccc;padding:2px 6px;background:#f8f8f8;font-weight:bold;">False</th></tr></thead><tbody><tr><td style="border:1px solid #ccc;padding:2px 6px;vertical-align:top;">truthy</td><td style="border:1px solid #ccc;padding:2px 6px;vertical-align:top;"></td></tr><tr><td style="border:1px solid #ccc;padding:2px 6px;vertical-align:top;"></td><td style="border:1px solid #ccc;padding:2px 6px;vertical-align:top;">falsey</td></tr></tbody></table>
|
@@ -0,0 +1 @@
|
|
1
|
+
<table><thead><tr><th>2.5</th></tr></thead><tbody><tr><td>two point five</td></tr></tbody></table>
|
@@ -0,0 +1 @@
|
|
1
|
+
<table style="border-collapse:collapse;font-size:12px;line-height:1.2;margin:0;padding:0;width:auto;"><thead><tr><th style="border:1px solid #ccc;padding:2px 6px;background:#f8f8f8;font-weight:bold;">2.5</th></tr></thead><tbody><tr><td style="border:1px solid #ccc;padding:2px 6px;vertical-align:top;">two point five</td></tr></tbody></table>
|
@@ -0,0 +1 @@
|
|
1
|
+
<table><thead><tr><th>1</th></tr></thead><tbody><tr><td>one</td></tr></tbody></table>
|
@@ -0,0 +1 @@
|
|
1
|
+
<table style="border-collapse:collapse;font-size:12px;line-height:1.2;margin:0;padding:0;width:auto;"><thead><tr><th style="border:1px solid #ccc;padding:2px 6px;background:#f8f8f8;font-weight:bold;">1</th></tr></thead><tbody><tr><td style="border:1px solid #ccc;padding:2px 6px;vertical-align:top;">one</td></tr></tbody></table>
|
@@ -0,0 +1,45 @@
|
|
1
|
+
import os
|
2
|
+
|
3
|
+
import pytest
|
4
|
+
|
5
|
+
from sfq import SFAuth
|
6
|
+
|
7
|
+
|
8
|
+
@pytest.fixture(scope="module")
|
9
|
+
def sf_instance():
|
10
|
+
required_env_vars = [
|
11
|
+
"SF_INSTANCE_URL",
|
12
|
+
"SF_CLIENT_ID",
|
13
|
+
"SF_CLIENT_SECRET",
|
14
|
+
"SF_REFRESH_TOKEN",
|
15
|
+
]
|
16
|
+
|
17
|
+
missing_vars = [var for var in required_env_vars if not os.getenv(var)]
|
18
|
+
if missing_vars:
|
19
|
+
pytest.fail(f"Missing required env vars: {', '.join(missing_vars)}")
|
20
|
+
|
21
|
+
sf = SFAuth(
|
22
|
+
instance_url=os.getenv("SF_INSTANCE_URL"),
|
23
|
+
client_id=os.getenv("SF_CLIENT_ID"),
|
24
|
+
client_secret=os.getenv("SF_CLIENT_SECRET").strip(),
|
25
|
+
refresh_token=os.getenv("SF_REFRESH_TOKEN"),
|
26
|
+
)
|
27
|
+
return sf
|
28
|
+
|
29
|
+
|
30
|
+
def test_html_table_conversion(sf_instance):
|
31
|
+
"""
|
32
|
+
Test the HTML table conversion utility.
|
33
|
+
"""
|
34
|
+
records = sf_instance.query("SELECT Id,NamespacePrefix FROM Organization LIMIT 1")
|
35
|
+
|
36
|
+
plain_html_table = sf_instance.records_to_html_table(records, styled=False)
|
37
|
+
styled_html_table = sf_instance.records_to_html_table(records, styled=True)
|
38
|
+
|
39
|
+
assert isinstance(plain_html_table, str)
|
40
|
+
assert "<table>" in plain_html_table
|
41
|
+
assert "<tr>" in plain_html_table
|
42
|
+
assert "<td>" in plain_html_table
|
43
|
+
org_id = sf_instance.org_id
|
44
|
+
assert f"<table><thead><tr><th>Id</th><th>NamespacePrefix</th></tr></thead><tbody><tr><td>{org_id}</td><td></td></tr></tbody></table>" == plain_html_table
|
45
|
+
assert f'<table style="border-collapse:collapse;font-size:12px;line-height:1.2;margin:0;padding:0;width:auto;"><thead><tr><th style="border:1px solid #ccc;padding:2px 6px;background:#f8f8f8;font-weight:bold;">Id</th><th style="border:1px solid #ccc;padding:2px 6px;background:#f8f8f8;font-weight:bold;">NamespacePrefix</th></tr></thead><tbody><tr><td style="border:1px solid #ccc;padding:2px 6px;vertical-align:top;">{org_id}</td><td style="border:1px solid #ccc;padding:2px 6px;vertical-align:top;"></td></tr></tbody></table>' == styled_html_table
|
@@ -0,0 +1,190 @@
|
|
1
|
+
import pytest
|
2
|
+
|
3
|
+
from sfq.utils import dicts_to_html_table
|
4
|
+
|
5
|
+
|
6
|
+
def test_typecastable_keys():
|
7
|
+
# int key
|
8
|
+
data_int = [{1: "one"}]
|
9
|
+
with open(
|
10
|
+
"./tests/html/test_typecastable_keys_int.html", "r", encoding="utf-8"
|
11
|
+
) as f:
|
12
|
+
expected = f.read()
|
13
|
+
with open(
|
14
|
+
"./tests/html/test_typecastable_keys_int_styled.html", "r", encoding="utf-8"
|
15
|
+
) as f:
|
16
|
+
expected_styled = f.read()
|
17
|
+
assert dicts_to_html_table(data_int) == expected
|
18
|
+
assert dicts_to_html_table(data_int, styled=True) == expected_styled
|
19
|
+
|
20
|
+
# float key
|
21
|
+
data_float = [{2.5: "two point five"}]
|
22
|
+
with open(
|
23
|
+
"./tests/html/test_typecastable_keys_float.html", "r", encoding="utf-8"
|
24
|
+
) as f:
|
25
|
+
expected = f.read()
|
26
|
+
with open(
|
27
|
+
"./tests/html/test_typecastable_keys_float_styled.html", "r", encoding="utf-8"
|
28
|
+
) as f:
|
29
|
+
expected_styled = f.read()
|
30
|
+
assert dicts_to_html_table(data_float) == expected
|
31
|
+
assert dicts_to_html_table(data_float, styled=True) == expected_styled
|
32
|
+
|
33
|
+
# bool key
|
34
|
+
data_bool = [{True: "truthy"}, {False: "falsey"}]
|
35
|
+
with open(
|
36
|
+
"./tests/html/test_typecastable_keys_bool.html", "r", encoding="utf-8"
|
37
|
+
) as f:
|
38
|
+
expected = f.read()
|
39
|
+
with open(
|
40
|
+
"./tests/html/test_typecastable_keys_bool_styled.html", "r", encoding="utf-8"
|
41
|
+
) as f:
|
42
|
+
expected_styled = f.read()
|
43
|
+
assert dicts_to_html_table(data_bool) == expected
|
44
|
+
assert dicts_to_html_table(data_bool, styled=True) == expected_styled
|
45
|
+
|
46
|
+
|
47
|
+
def test_empty_list():
|
48
|
+
with open("./tests/html/test_empty_list.html", "r", encoding="utf-8") as f:
|
49
|
+
expected = f.read()
|
50
|
+
with open("./tests/html/test_empty_list_styled.html", "r", encoding="utf-8") as f:
|
51
|
+
expected_styled = f.read()
|
52
|
+
assert dicts_to_html_table([]) == expected
|
53
|
+
assert dicts_to_html_table([], styled=True) == expected_styled
|
54
|
+
|
55
|
+
|
56
|
+
def test_single_flat_dict():
|
57
|
+
data = [{"a": "b", "c": "d"}]
|
58
|
+
|
59
|
+
with open("./tests/html/test_single_flat_dict.html", "r", encoding="utf-8") as f:
|
60
|
+
expected = f.read()
|
61
|
+
with open(
|
62
|
+
"./tests/html/test_single_flat_dict_styled.html", "r", encoding="utf-8"
|
63
|
+
) as f:
|
64
|
+
expected_styled = f.read()
|
65
|
+
assert dicts_to_html_table(data) == expected
|
66
|
+
assert dicts_to_html_table(data, styled=True) == expected_styled
|
67
|
+
|
68
|
+
|
69
|
+
def test_multiple_dicts():
|
70
|
+
data = [{"x": "y"}, {"z": "w"}]
|
71
|
+
|
72
|
+
with open("./tests/html/test_multiple_dicts.html", "r", encoding="utf-8") as f:
|
73
|
+
expected = f.read()
|
74
|
+
with open(
|
75
|
+
"./tests/html/test_multiple_dicts_styled.html", "r", encoding="utf-8"
|
76
|
+
) as f:
|
77
|
+
expected_styled = f.read()
|
78
|
+
assert dicts_to_html_table(data) == expected
|
79
|
+
assert dicts_to_html_table(data, styled=True) == expected_styled
|
80
|
+
|
81
|
+
|
82
|
+
def test_none_value():
|
83
|
+
data = [{"foo": None}]
|
84
|
+
|
85
|
+
with open("./tests/html/test_none_value.html", "r", encoding="utf-8") as f:
|
86
|
+
expected = f.read()
|
87
|
+
with open("./tests/html/test_none_value_styled.html", "r", encoding="utf-8") as f:
|
88
|
+
expected_styled = f.read()
|
89
|
+
assert dicts_to_html_table(data) == expected
|
90
|
+
assert dicts_to_html_table(data, styled=True) == expected_styled
|
91
|
+
|
92
|
+
|
93
|
+
def test_int_float_bool():
|
94
|
+
data = [{"i": 1, "f": 2.5, "b": True}]
|
95
|
+
|
96
|
+
with open("./tests/html/test_int_float_bool.html", "r", encoding="utf-8") as f:
|
97
|
+
expected = f.read()
|
98
|
+
with open(
|
99
|
+
"./tests/html/test_int_float_bool_styled.html", "r", encoding="utf-8"
|
100
|
+
) as f:
|
101
|
+
expected_styled = f.read()
|
102
|
+
assert dicts_to_html_table(data) == expected
|
103
|
+
assert dicts_to_html_table(data, styled=True) == expected_styled
|
104
|
+
|
105
|
+
|
106
|
+
def test_list_value():
|
107
|
+
data = [{"hello": ["bob", "sally"]}]
|
108
|
+
with open("./tests/html/test_list_value.html", "r", encoding="utf-8") as f:
|
109
|
+
expected = f.read()
|
110
|
+
with open("./tests/html/test_list_value_styled.html", "r", encoding="utf-8") as f:
|
111
|
+
expected_styled = f.read()
|
112
|
+
assert dicts_to_html_table(data) == expected
|
113
|
+
assert dicts_to_html_table(data, styled=True) == expected_styled
|
114
|
+
|
115
|
+
|
116
|
+
def test_nested_dict():
|
117
|
+
data = [{"outer": {"inner": "value"}}]
|
118
|
+
data = [{"outer": {"inner": "value"}}]
|
119
|
+
with open("./tests/html/test_nested_dict.html", "r", encoding="utf-8") as f:
|
120
|
+
expected = f.read()
|
121
|
+
with open("./tests/html/test_nested_dict_styled.html", "r", encoding="utf-8") as f:
|
122
|
+
expected_styled = f.read()
|
123
|
+
assert dicts_to_html_table(data) == expected
|
124
|
+
assert dicts_to_html_table(data, styled=True) == expected_styled
|
125
|
+
|
126
|
+
|
127
|
+
def test_complex_nested():
|
128
|
+
data = [{"a": [1, {"b": [2, 3]}, None]}]
|
129
|
+
data = [{"a": [1, {"b": [2, 3]}, None]}]
|
130
|
+
with open("./tests/html/test_complex_nested.html", "r", encoding="utf-8") as f:
|
131
|
+
expected = f.read()
|
132
|
+
with open(
|
133
|
+
"./tests/html/test_complex_nested_styled.html", "r", encoding="utf-8"
|
134
|
+
) as f:
|
135
|
+
expected_styled = f.read()
|
136
|
+
assert dicts_to_html_table(data) == expected
|
137
|
+
assert dicts_to_html_table(data, styled=True) == expected_styled
|
138
|
+
|
139
|
+
|
140
|
+
def test_invalid_input_type():
|
141
|
+
# Not printing for error cases
|
142
|
+
with pytest.raises(ValueError):
|
143
|
+
dicts_to_html_table("not a list")
|
144
|
+
with pytest.raises(ValueError):
|
145
|
+
dicts_to_html_table(["not a dict"])
|
146
|
+
# Only non-typecastable keys should raise ValueError
|
147
|
+
with pytest.raises(ValueError):
|
148
|
+
dicts_to_html_table([{(1, 2): "value"}])
|
149
|
+
with pytest.raises(ValueError):
|
150
|
+
dicts_to_html_table([{object(): "value"}])
|
151
|
+
# Also test styled param does not affect error
|
152
|
+
with pytest.raises(ValueError):
|
153
|
+
dicts_to_html_table("not a list", styled=True)
|
154
|
+
with pytest.raises(ValueError):
|
155
|
+
dicts_to_html_table(["not a dict"], styled=True)
|
156
|
+
with pytest.raises(ValueError):
|
157
|
+
dicts_to_html_table([{(1, 2): "value"}], styled=True)
|
158
|
+
with pytest.raises(ValueError):
|
159
|
+
dicts_to_html_table([{object(): "value"}], styled=True)
|
160
|
+
|
161
|
+
|
162
|
+
def test_other_types():
|
163
|
+
class Custom:
|
164
|
+
def __str__(self):
|
165
|
+
return "custom_str"
|
166
|
+
|
167
|
+
data = [{"custom": Custom()}]
|
168
|
+
with open("./tests/html/test_other_types.html", "r", encoding="utf-8") as f:
|
169
|
+
expected = f.read()
|
170
|
+
with open("./tests/html/test_other_types_styled.html", "r", encoding="utf-8") as f:
|
171
|
+
expected_styled = f.read()
|
172
|
+
assert dicts_to_html_table(data) == expected
|
173
|
+
assert dicts_to_html_table(data, styled=True) == expected_styled
|
174
|
+
|
175
|
+
|
176
|
+
def test_sample_report():
|
177
|
+
data = [
|
178
|
+
{"name": "Sample Report", "date": "2023-01-01", "status": "Completed"},
|
179
|
+
{"name": "Sample Report 2", "date": "2023-01-02", "status": "In Progress"},
|
180
|
+
{"name": "Sample Report 3", "date": "2023-01-03", "status": "Not Started"},
|
181
|
+
]
|
182
|
+
expected = "<table><tbody><tr><td>name</td><td>Sample Report</td></tr><tr><td>date</td><td>2023-01-01</td></tr><tr><td>status</td><td>Completed</td></tr></tbody></table>"
|
183
|
+
with open("./tests/html/test_sample_report.html", "r", encoding="utf-8") as f:
|
184
|
+
expected = f.read()
|
185
|
+
with open(
|
186
|
+
"./tests/html/test_sample_report_styled.html", "r", encoding="utf-8"
|
187
|
+
) as f:
|
188
|
+
expected_styled = f.read()
|
189
|
+
assert dicts_to_html_table(data) == expected
|
190
|
+
assert dicts_to_html_table(data, styled=True) == expected_styled
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|