sfq 0.0.36__tar.gz → 0.0.38__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.36 → sfq-0.0.38}/.github/workflows/publish.yml +2 -2
- {sfq-0.0.36 → sfq-0.0.38}/PKG-INFO +1 -1
- {sfq-0.0.36 → sfq-0.0.38}/pyproject.toml +1 -1
- {sfq-0.0.36 → sfq-0.0.38}/src/sfq/__init__.py +5 -4
- {sfq-0.0.36 → sfq-0.0.38}/src/sfq/http_client.py +1 -1
- {sfq-0.0.36 → sfq-0.0.38}/src/sfq/utils.py +69 -14
- sfq-0.0.38/tests/test_records_to_html.py +153 -0
- {sfq-0.0.36 → sfq-0.0.38}/uv.lock +1 -1
- sfq-0.0.36/tests/test_records_to_html.py +0 -45
- {sfq-0.0.36 → sfq-0.0.38}/.gitignore +0 -0
- {sfq-0.0.36 → sfq-0.0.38}/.python-version +0 -0
- {sfq-0.0.36 → sfq-0.0.38}/README.md +0 -0
- {sfq-0.0.36 → sfq-0.0.38}/src/sfq/_cometd.py +0 -0
- {sfq-0.0.36 → sfq-0.0.38}/src/sfq/auth.py +0 -0
- {sfq-0.0.36 → sfq-0.0.38}/src/sfq/crud.py +0 -0
- {sfq-0.0.36 → sfq-0.0.38}/src/sfq/debug_cleanup.py +0 -0
- {sfq-0.0.36 → sfq-0.0.38}/src/sfq/exceptions.py +0 -0
- {sfq-0.0.36 → sfq-0.0.38}/src/sfq/py.typed +0 -0
- {sfq-0.0.36 → sfq-0.0.38}/src/sfq/query.py +0 -0
- {sfq-0.0.36 → sfq-0.0.38}/src/sfq/soap.py +0 -0
- {sfq-0.0.36 → sfq-0.0.38}/tests/html/test_complex_nested.html +0 -0
- {sfq-0.0.36 → sfq-0.0.38}/tests/html/test_complex_nested_styled.html +0 -0
- {sfq-0.0.36 → sfq-0.0.38}/tests/html/test_empty_list.html +0 -0
- {sfq-0.0.36 → sfq-0.0.38}/tests/html/test_empty_list_styled.html +0 -0
- {sfq-0.0.36 → sfq-0.0.38}/tests/html/test_int_float_bool.html +0 -0
- {sfq-0.0.36 → sfq-0.0.38}/tests/html/test_int_float_bool_styled.html +0 -0
- {sfq-0.0.36 → sfq-0.0.38}/tests/html/test_list_value.html +0 -0
- {sfq-0.0.36 → sfq-0.0.38}/tests/html/test_list_value_styled.html +0 -0
- {sfq-0.0.36 → sfq-0.0.38}/tests/html/test_multiple_dicts.html +0 -0
- {sfq-0.0.36 → sfq-0.0.38}/tests/html/test_multiple_dicts_styled.html +0 -0
- {sfq-0.0.36 → sfq-0.0.38}/tests/html/test_nested_dict.html +0 -0
- {sfq-0.0.36 → sfq-0.0.38}/tests/html/test_nested_dict_styled.html +0 -0
- {sfq-0.0.36 → sfq-0.0.38}/tests/html/test_none_value.html +0 -0
- {sfq-0.0.36 → sfq-0.0.38}/tests/html/test_none_value_styled.html +0 -0
- {sfq-0.0.36 → sfq-0.0.38}/tests/html/test_other_types.html +0 -0
- {sfq-0.0.36 → sfq-0.0.38}/tests/html/test_other_types_styled.html +0 -0
- {sfq-0.0.36 → sfq-0.0.38}/tests/html/test_sample_report.html +0 -0
- {sfq-0.0.36 → sfq-0.0.38}/tests/html/test_sample_report_styled.html +0 -0
- {sfq-0.0.36 → sfq-0.0.38}/tests/html/test_single_flat_dict.html +0 -0
- {sfq-0.0.36 → sfq-0.0.38}/tests/html/test_single_flat_dict_styled.html +0 -0
- {sfq-0.0.36 → sfq-0.0.38}/tests/html/test_typecastable_keys_bool.html +0 -0
- {sfq-0.0.36 → sfq-0.0.38}/tests/html/test_typecastable_keys_bool_styled.html +0 -0
- {sfq-0.0.36 → sfq-0.0.38}/tests/html/test_typecastable_keys_float.html +0 -0
- {sfq-0.0.36 → sfq-0.0.38}/tests/html/test_typecastable_keys_float_styled.html +0 -0
- {sfq-0.0.36 → sfq-0.0.38}/tests/html/test_typecastable_keys_int.html +0 -0
- {sfq-0.0.36 → sfq-0.0.38}/tests/html/test_typecastable_keys_int_styled.html +0 -0
- {sfq-0.0.36 → sfq-0.0.38}/tests/test_auth.py +0 -0
- {sfq-0.0.36 → sfq-0.0.38}/tests/test_cdelete.py +0 -0
- {sfq-0.0.36 → sfq-0.0.38}/tests/test_compatibility.py +0 -0
- {sfq-0.0.36 → sfq-0.0.38}/tests/test_cquery.py +0 -0
- {sfq-0.0.36 → sfq-0.0.38}/tests/test_create.py +0 -0
- {sfq-0.0.36 → sfq-0.0.38}/tests/test_crud.py +0 -0
- {sfq-0.0.36 → sfq-0.0.38}/tests/test_crud_e2e.py +0 -0
- {sfq-0.0.36 → sfq-0.0.38}/tests/test_cupdate.py +0 -0
- {sfq-0.0.36 → sfq-0.0.38}/tests/test_debug_cleanup_e2e.py +0 -0
- {sfq-0.0.36 → sfq-0.0.38}/tests/test_debug_cleanup_unit.py +0 -0
- {sfq-0.0.36 → sfq-0.0.38}/tests/test_http_client.py +0 -0
- {sfq-0.0.36 → sfq-0.0.38}/tests/test_limits_api.py +0 -0
- {sfq-0.0.36 → sfq-0.0.38}/tests/test_log_trace_redact.py +0 -0
- {sfq-0.0.36 → sfq-0.0.38}/tests/test_open_frontdoor.py +0 -0
- {sfq-0.0.36 → sfq-0.0.38}/tests/test_query.py +0 -0
- {sfq-0.0.36 → sfq-0.0.38}/tests/test_query_client.py +0 -0
- {sfq-0.0.36 → sfq-0.0.38}/tests/test_query_e2e.py +0 -0
- {sfq-0.0.36 → sfq-0.0.38}/tests/test_query_integration.py +0 -0
- {sfq-0.0.36 → sfq-0.0.38}/tests/test_soap.py +0 -0
- {sfq-0.0.36 → sfq-0.0.38}/tests/test_soap_batch_operation.py +0 -0
- {sfq-0.0.36 → sfq-0.0.38}/tests/test_static_resources.py +0 -0
- {sfq-0.0.36 → sfq-0.0.38}/tests/test_utils.py +0 -0
- {sfq-0.0.36 → sfq-0.0.38}/tests/test_utils_html_table.py +0 -0
@@ -76,7 +76,6 @@ jobs:
|
|
76
76
|
|
77
77
|
echo "Updating src/sfq/__init__.py user_agent to $VERSION"
|
78
78
|
sed -i -E "s/(user_agent: str = \"sfq\/)[0-9]+\.[0-9]+\.[0-9]+(\")/\1$VERSION\2/" src/sfq/__init__.py
|
79
|
-
sed -i -E "s/(user_agent: str = \"sfq\/)[0-9]+\.[0-9]+\.[0-9]+(\")/\1$VERSION\2/" src/sfq/http_client.py
|
80
79
|
sed -i -E "s/(default is \"sfq\/)[0-9]+\.[0-9]+\.[0-9]+(\")/\1$VERSION\2/" src/sfq/__init__.py
|
81
80
|
|
82
81
|
echo "Updating src/sfq/__init__.py __version__ to $VERSION"
|
@@ -85,6 +84,7 @@ jobs:
|
|
85
84
|
|
86
85
|
echo "Updating src/sfq/http_client.py user_agent to $VERSION"
|
87
86
|
sed -i -E "s/(user_agent: str = \"sfq\/)[0-9]+\.[0-9]+\.[0-9]+(\")/\1$VERSION\2/" src/sfq/http_client.py
|
87
|
+
sed -i -E "s/(user_agent: str = \"sfq\/)[0-9]+\.[0-9]+\.[0-9]+(\",)/\1$VERSION\2/" src/sfq/http_client.py
|
88
88
|
|
89
89
|
- name: Run tests
|
90
90
|
run: pytest --verbose --strict-config
|
@@ -99,7 +99,7 @@ jobs:
|
|
99
99
|
run: |
|
100
100
|
git config user.name "github-actions[bot]"
|
101
101
|
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
102
|
-
git add pyproject.toml uv.lock src/sfq/__init__.py
|
102
|
+
git add pyproject.toml uv.lock src/sfq/__init__.py src/sfq/http_client.py
|
103
103
|
git commit -m "CI: bump version to ${{ steps.get_version.outputs.version }}"
|
104
104
|
git push
|
105
105
|
|
@@ -43,7 +43,7 @@ __all__ = [
|
|
43
43
|
"__version__",
|
44
44
|
]
|
45
45
|
|
46
|
-
__version__ = "0.0.
|
46
|
+
__version__ = "0.0.38"
|
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.38",
|
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.38"
|
136
136
|
"""
|
137
137
|
### `__version__`
|
138
138
|
|
@@ -565,6 +565,7 @@ class SFAuth:
|
|
565
565
|
def records_to_html_table(
|
566
566
|
self,
|
567
567
|
items: List[Dict[str, Any]],
|
568
|
+
headers: Dict[str, str] = None,
|
568
569
|
styled: bool = False,
|
569
570
|
) -> str:
|
570
571
|
"""
|
@@ -576,4 +577,4 @@ class SFAuth:
|
|
576
577
|
"""
|
577
578
|
if "records" in items:
|
578
579
|
items = items["records"]
|
579
|
-
return records_to_html_table(items, styled=styled)
|
580
|
+
return records_to_html_table(items, headers=headers, styled=styled)
|
@@ -197,9 +197,7 @@ def extract_org_and_user_ids(token_id_url: str) -> Tuple[str, str]:
|
|
197
197
|
raise ValueError(f"Invalid token ID URL format: {token_id_url}")
|
198
198
|
|
199
199
|
|
200
|
-
def dicts_to_html_table(
|
201
|
-
items: List[Dict[str, Any]], styled: bool = False
|
202
|
-
) -> str:
|
200
|
+
def dicts_to_html_table(items: List[Dict[str, Any]], styled: bool = False) -> str:
|
203
201
|
"""
|
204
202
|
Convert a list of dictionaries to a compact HTML table.
|
205
203
|
|
@@ -215,7 +213,7 @@ def dicts_to_html_table(
|
|
215
213
|
if val is None:
|
216
214
|
return ""
|
217
215
|
if isinstance(val, (int, float, str, bool)):
|
218
|
-
return
|
216
|
+
return str(val)
|
219
217
|
if isinstance(val, list):
|
220
218
|
return (
|
221
219
|
"<ul>"
|
@@ -277,7 +275,7 @@ def dicts_to_html_table(
|
|
277
275
|
)
|
278
276
|
if columns:
|
279
277
|
html = [f'<table style="{table_style}"><thead><tr>']
|
280
|
-
html.extend(f'<th style="{th_style}">{
|
278
|
+
html.extend(f'<th style="{th_style}">{col}</th>' for col in columns)
|
281
279
|
html.append("</tr></thead><tbody>")
|
282
280
|
for d in items:
|
283
281
|
html.append("<tr>")
|
@@ -294,7 +292,7 @@ def dicts_to_html_table(
|
|
294
292
|
else:
|
295
293
|
if columns:
|
296
294
|
html = ["<table><thead><tr>"]
|
297
|
-
html.extend(f"<th>{
|
295
|
+
html.extend(f"<th>{col}</th>" for col in columns)
|
298
296
|
html.append("</tr></thead><tbody>")
|
299
297
|
for d in items:
|
300
298
|
html.append("<tr>")
|
@@ -309,13 +307,70 @@ def dicts_to_html_table(
|
|
309
307
|
|
310
308
|
return "".join(html)
|
311
309
|
|
312
|
-
|
313
|
-
|
314
|
-
|
315
|
-
|
316
|
-
for
|
310
|
+
|
311
|
+
def flatten_dict(d, parent_key="", sep="."):
|
312
|
+
"""Recursively flatten a dictionary with dot notation."""
|
313
|
+
items = {}
|
314
|
+
for k, v in d.items():
|
315
|
+
new_key = f"{parent_key}{sep}{k}" if parent_key else k
|
316
|
+
if isinstance(v, dict):
|
317
|
+
items.update(flatten_dict(v, new_key, sep=sep))
|
318
|
+
else:
|
319
|
+
items[new_key] = v
|
320
|
+
return items
|
321
|
+
|
322
|
+
|
323
|
+
def remove_attributes(obj):
|
324
|
+
"""Recursively remove 'attributes' key from dicts/lists."""
|
325
|
+
if isinstance(obj, dict):
|
326
|
+
return {k: remove_attributes(v) for k, v in obj.items() if k != "attributes"}
|
327
|
+
elif isinstance(obj, list):
|
328
|
+
return [remove_attributes(item) for item in obj]
|
329
|
+
else:
|
330
|
+
return obj
|
331
|
+
|
332
|
+
|
333
|
+
def records_to_html_table(
|
334
|
+
records: List[Dict[str, Any]], headers: Dict[str, str] = None, styled: bool = False
|
335
|
+
) -> str:
|
336
|
+
if not isinstance(records, list):
|
337
|
+
raise ValueError("records must be a list of dictionaries")
|
338
|
+
|
339
|
+
cleaned = remove_attributes(records)
|
340
|
+
|
341
|
+
flat_rows = []
|
342
|
+
for record in cleaned:
|
317
343
|
if not isinstance(record, dict):
|
318
344
|
raise ValueError(f"Record is not a dictionary: {record!r}")
|
319
|
-
|
320
|
-
|
321
|
-
|
345
|
+
flat_rows.append(flatten_dict(record))
|
346
|
+
|
347
|
+
# Preserve column order across all rows
|
348
|
+
seen = set()
|
349
|
+
ordered_columns = []
|
350
|
+
for row in flat_rows:
|
351
|
+
for key in row.keys():
|
352
|
+
if key not in seen:
|
353
|
+
ordered_columns.append(key)
|
354
|
+
seen.add(key)
|
355
|
+
|
356
|
+
# headers optionally remaps flattened field names to user-friendly display names
|
357
|
+
if headers is None:
|
358
|
+
headers = {}
|
359
|
+
for col in ordered_columns:
|
360
|
+
headers[col] = col
|
361
|
+
else:
|
362
|
+
for col in ordered_columns:
|
363
|
+
headers[col] = headers.get(col, col)
|
364
|
+
|
365
|
+
# Normalize rows so all have the same keys, using remapped column names
|
366
|
+
normalized_data = []
|
367
|
+
for row in flat_rows:
|
368
|
+
normalized_row = {
|
369
|
+
headers.get(col, col): (
|
370
|
+
"" if row.get(col, None) is None else row.get(col, "")
|
371
|
+
)
|
372
|
+
for col in ordered_columns
|
373
|
+
}
|
374
|
+
normalized_data.append(normalized_row)
|
375
|
+
|
376
|
+
return dicts_to_html_table(normalized_data, styled=styled)
|
@@ -0,0 +1,153 @@
|
|
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
|
46
|
+
|
47
|
+
def test_html_table_with_nested_fields(sf_instance):
|
48
|
+
results = sf_instance.query("SELECT Id,Name,CreatedBy.Name,CreatedBy.Profile.Name FROM User WHERE CreatedBy.Name <> null LIMIT 1")
|
49
|
+
html = sf_instance.records_to_html_table(results)
|
50
|
+
assert isinstance(html, str)
|
51
|
+
assert "<table>" in html
|
52
|
+
assert "<tr>" in html
|
53
|
+
assert "<td>" in html
|
54
|
+
assert "CreatedBy.Name" in html
|
55
|
+
assert "CreatedBy.Profile.Name" in html
|
56
|
+
assert "Id" in html
|
57
|
+
|
58
|
+
def test_html_table_with_mapped_headers(sf_instance):
|
59
|
+
header_map = {
|
60
|
+
"Id": "Id",
|
61
|
+
"Name": "Name",
|
62
|
+
"CreatedBy.Name": "Created By",
|
63
|
+
"CreatedBy.Profile.Name": "Created By Profile"
|
64
|
+
}
|
65
|
+
results = sf_instance.query("SELECT Id,Name,CreatedBy.Name,CreatedBy.Profile.Name FROM User WHERE CreatedBy.Name <> null LIMIT 1")
|
66
|
+
html = sf_instance.records_to_html_table(results, headers=header_map)
|
67
|
+
assert isinstance(html, str)
|
68
|
+
assert "<table>" in html
|
69
|
+
assert "<tr>" in html
|
70
|
+
assert "<td>" in html
|
71
|
+
assert "Created By" in html
|
72
|
+
assert "Created By Profile" in html
|
73
|
+
assert "Id" in html
|
74
|
+
assert "CreatedBy.Name" not in html
|
75
|
+
assert "CreatedBy.Profile.Name" not in html
|
76
|
+
|
77
|
+
def test_html_table_with_no_records(sf_instance):
|
78
|
+
html = sf_instance.records_to_html_table([])
|
79
|
+
assert isinstance(html, str)
|
80
|
+
assert "<table>" in html
|
81
|
+
assert "<tr>" not in html
|
82
|
+
assert "<td>" not in html
|
83
|
+
assert "</table>" in html
|
84
|
+
|
85
|
+
def test_html_table_with_single_record(sf_instance):
|
86
|
+
records = sf_instance.query("SELECT Id,NamespacePrefix FROM Organization LIMIT 1")
|
87
|
+
html = sf_instance.records_to_html_table(records)
|
88
|
+
assert isinstance(html, str)
|
89
|
+
assert "<table>" in html
|
90
|
+
assert "<tr>" in html
|
91
|
+
assert "<td>" in html
|
92
|
+
org_id = sf_instance.org_id
|
93
|
+
assert f"<table><thead><tr><th>Id</th><th>NamespacePrefix</th></tr></thead><tbody><tr><td>{org_id}</td><td></td></tr></tbody></table>" == html
|
94
|
+
|
95
|
+
styled_html = sf_instance.records_to_html_table(records, styled=True)
|
96
|
+
assert isinstance(styled_html, str)
|
97
|
+
assert "<table" in styled_html
|
98
|
+
assert 'style="border-collapse:collapse;font-size:12px;line-height:1.2;margin:0;padding:0;width:auto;"' in styled_html
|
99
|
+
assert "<tr" in styled_html
|
100
|
+
assert "<td" in styled_html
|
101
|
+
assert "</td>" in styled_html
|
102
|
+
assert "</tr>" in styled_html
|
103
|
+
assert "</table>" in styled_html
|
104
|
+
|
105
|
+
def test_html_with_nested_fields(sf_instance):
|
106
|
+
results = sf_instance.query("SELECT Id,Name,CreatedBy.Name,CreatedBy.Profile.Name FROM User WHERE CreatedBy.Name <> null LIMIT 1")
|
107
|
+
html = sf_instance.records_to_html_table(results)
|
108
|
+
assert isinstance(html, str)
|
109
|
+
assert "<table>" in html
|
110
|
+
assert "<tr" in html
|
111
|
+
assert "<td" in html
|
112
|
+
assert "</tr>" in html
|
113
|
+
assert "</td>" in html
|
114
|
+
assert "CreatedBy.Name" in html
|
115
|
+
assert "CreatedBy.Profile.Name" in html
|
116
|
+
assert "Id" in html
|
117
|
+
|
118
|
+
|
119
|
+
def test_html_table_with_none_values(sf_instance):
|
120
|
+
records = [
|
121
|
+
{"Id": None, "Name": "Test"},
|
122
|
+
{"Id": "123", "Name": None},
|
123
|
+
]
|
124
|
+
html = sf_instance.records_to_html_table(records)
|
125
|
+
assert "<td></td>" in html # None should be rendered as empty
|
126
|
+
assert "Test" in html
|
127
|
+
assert "123" in html
|
128
|
+
|
129
|
+
def test_html_table_with_empty_dict(sf_instance):
|
130
|
+
records = [{}]
|
131
|
+
html = sf_instance.records_to_html_table(records)
|
132
|
+
assert "<table>" in html
|
133
|
+
assert "<tr>" not in html or "<td>" not in html
|
134
|
+
|
135
|
+
def test_html_table_with_mixed_types(sf_instance):
|
136
|
+
records = [
|
137
|
+
{"Id": 1, "Active": True, "Score": 99.5},
|
138
|
+
{"Id": 2, "Active": False, "Score": None},
|
139
|
+
]
|
140
|
+
html = sf_instance.records_to_html_table(records)
|
141
|
+
assert "1" in html
|
142
|
+
assert "True" in html or "False" in html
|
143
|
+
assert "99.5" in html
|
144
|
+
|
145
|
+
def test_html_table_with_header_remapping_missing_keys(sf_instance):
|
146
|
+
records = [
|
147
|
+
{"Id": "abc", "Name": "Test", "Extra": "Value"}
|
148
|
+
]
|
149
|
+
headers = {"Id": "Identifier", "Name": "Label"} # 'Extra' not mapped
|
150
|
+
html = sf_instance.records_to_html_table(records, headers=headers)
|
151
|
+
assert "Identifier" in html
|
152
|
+
assert "Label" in html
|
153
|
+
assert "Extra" in html # Should fall back to original key
|
@@ -1,45 +0,0 @@
|
|
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
|
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
|
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
|