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.
Files changed (69) hide show
  1. {sfq-0.0.36 → sfq-0.0.38}/.github/workflows/publish.yml +2 -2
  2. {sfq-0.0.36 → sfq-0.0.38}/PKG-INFO +1 -1
  3. {sfq-0.0.36 → sfq-0.0.38}/pyproject.toml +1 -1
  4. {sfq-0.0.36 → sfq-0.0.38}/src/sfq/__init__.py +5 -4
  5. {sfq-0.0.36 → sfq-0.0.38}/src/sfq/http_client.py +1 -1
  6. {sfq-0.0.36 → sfq-0.0.38}/src/sfq/utils.py +69 -14
  7. sfq-0.0.38/tests/test_records_to_html.py +153 -0
  8. {sfq-0.0.36 → sfq-0.0.38}/uv.lock +1 -1
  9. sfq-0.0.36/tests/test_records_to_html.py +0 -45
  10. {sfq-0.0.36 → sfq-0.0.38}/.gitignore +0 -0
  11. {sfq-0.0.36 → sfq-0.0.38}/.python-version +0 -0
  12. {sfq-0.0.36 → sfq-0.0.38}/README.md +0 -0
  13. {sfq-0.0.36 → sfq-0.0.38}/src/sfq/_cometd.py +0 -0
  14. {sfq-0.0.36 → sfq-0.0.38}/src/sfq/auth.py +0 -0
  15. {sfq-0.0.36 → sfq-0.0.38}/src/sfq/crud.py +0 -0
  16. {sfq-0.0.36 → sfq-0.0.38}/src/sfq/debug_cleanup.py +0 -0
  17. {sfq-0.0.36 → sfq-0.0.38}/src/sfq/exceptions.py +0 -0
  18. {sfq-0.0.36 → sfq-0.0.38}/src/sfq/py.typed +0 -0
  19. {sfq-0.0.36 → sfq-0.0.38}/src/sfq/query.py +0 -0
  20. {sfq-0.0.36 → sfq-0.0.38}/src/sfq/soap.py +0 -0
  21. {sfq-0.0.36 → sfq-0.0.38}/tests/html/test_complex_nested.html +0 -0
  22. {sfq-0.0.36 → sfq-0.0.38}/tests/html/test_complex_nested_styled.html +0 -0
  23. {sfq-0.0.36 → sfq-0.0.38}/tests/html/test_empty_list.html +0 -0
  24. {sfq-0.0.36 → sfq-0.0.38}/tests/html/test_empty_list_styled.html +0 -0
  25. {sfq-0.0.36 → sfq-0.0.38}/tests/html/test_int_float_bool.html +0 -0
  26. {sfq-0.0.36 → sfq-0.0.38}/tests/html/test_int_float_bool_styled.html +0 -0
  27. {sfq-0.0.36 → sfq-0.0.38}/tests/html/test_list_value.html +0 -0
  28. {sfq-0.0.36 → sfq-0.0.38}/tests/html/test_list_value_styled.html +0 -0
  29. {sfq-0.0.36 → sfq-0.0.38}/tests/html/test_multiple_dicts.html +0 -0
  30. {sfq-0.0.36 → sfq-0.0.38}/tests/html/test_multiple_dicts_styled.html +0 -0
  31. {sfq-0.0.36 → sfq-0.0.38}/tests/html/test_nested_dict.html +0 -0
  32. {sfq-0.0.36 → sfq-0.0.38}/tests/html/test_nested_dict_styled.html +0 -0
  33. {sfq-0.0.36 → sfq-0.0.38}/tests/html/test_none_value.html +0 -0
  34. {sfq-0.0.36 → sfq-0.0.38}/tests/html/test_none_value_styled.html +0 -0
  35. {sfq-0.0.36 → sfq-0.0.38}/tests/html/test_other_types.html +0 -0
  36. {sfq-0.0.36 → sfq-0.0.38}/tests/html/test_other_types_styled.html +0 -0
  37. {sfq-0.0.36 → sfq-0.0.38}/tests/html/test_sample_report.html +0 -0
  38. {sfq-0.0.36 → sfq-0.0.38}/tests/html/test_sample_report_styled.html +0 -0
  39. {sfq-0.0.36 → sfq-0.0.38}/tests/html/test_single_flat_dict.html +0 -0
  40. {sfq-0.0.36 → sfq-0.0.38}/tests/html/test_single_flat_dict_styled.html +0 -0
  41. {sfq-0.0.36 → sfq-0.0.38}/tests/html/test_typecastable_keys_bool.html +0 -0
  42. {sfq-0.0.36 → sfq-0.0.38}/tests/html/test_typecastable_keys_bool_styled.html +0 -0
  43. {sfq-0.0.36 → sfq-0.0.38}/tests/html/test_typecastable_keys_float.html +0 -0
  44. {sfq-0.0.36 → sfq-0.0.38}/tests/html/test_typecastable_keys_float_styled.html +0 -0
  45. {sfq-0.0.36 → sfq-0.0.38}/tests/html/test_typecastable_keys_int.html +0 -0
  46. {sfq-0.0.36 → sfq-0.0.38}/tests/html/test_typecastable_keys_int_styled.html +0 -0
  47. {sfq-0.0.36 → sfq-0.0.38}/tests/test_auth.py +0 -0
  48. {sfq-0.0.36 → sfq-0.0.38}/tests/test_cdelete.py +0 -0
  49. {sfq-0.0.36 → sfq-0.0.38}/tests/test_compatibility.py +0 -0
  50. {sfq-0.0.36 → sfq-0.0.38}/tests/test_cquery.py +0 -0
  51. {sfq-0.0.36 → sfq-0.0.38}/tests/test_create.py +0 -0
  52. {sfq-0.0.36 → sfq-0.0.38}/tests/test_crud.py +0 -0
  53. {sfq-0.0.36 → sfq-0.0.38}/tests/test_crud_e2e.py +0 -0
  54. {sfq-0.0.36 → sfq-0.0.38}/tests/test_cupdate.py +0 -0
  55. {sfq-0.0.36 → sfq-0.0.38}/tests/test_debug_cleanup_e2e.py +0 -0
  56. {sfq-0.0.36 → sfq-0.0.38}/tests/test_debug_cleanup_unit.py +0 -0
  57. {sfq-0.0.36 → sfq-0.0.38}/tests/test_http_client.py +0 -0
  58. {sfq-0.0.36 → sfq-0.0.38}/tests/test_limits_api.py +0 -0
  59. {sfq-0.0.36 → sfq-0.0.38}/tests/test_log_trace_redact.py +0 -0
  60. {sfq-0.0.36 → sfq-0.0.38}/tests/test_open_frontdoor.py +0 -0
  61. {sfq-0.0.36 → sfq-0.0.38}/tests/test_query.py +0 -0
  62. {sfq-0.0.36 → sfq-0.0.38}/tests/test_query_client.py +0 -0
  63. {sfq-0.0.36 → sfq-0.0.38}/tests/test_query_e2e.py +0 -0
  64. {sfq-0.0.36 → sfq-0.0.38}/tests/test_query_integration.py +0 -0
  65. {sfq-0.0.36 → sfq-0.0.38}/tests/test_soap.py +0 -0
  66. {sfq-0.0.36 → sfq-0.0.38}/tests/test_soap_batch_operation.py +0 -0
  67. {sfq-0.0.36 → sfq-0.0.38}/tests/test_static_resources.py +0 -0
  68. {sfq-0.0.36 → sfq-0.0.38}/tests/test_utils.py +0 -0
  69. {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
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sfq
3
- Version: 0.0.36
3
+ Version: 0.0.38
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,6 +1,6 @@
1
1
  [project]
2
2
  name = "sfq"
3
- version = "0.0.36"
3
+ version = "0.0.38"
4
4
  description = "Python wrapper for the Salesforce's Query API."
5
5
  readme = "README.md"
6
6
  authors = [{ name = "David Moruzzi", email = "sfq.pypi@dmoruzi.com" }]
@@ -43,7 +43,7 @@ __all__ = [
43
43
  "__version__",
44
44
  ]
45
45
 
46
- __version__ = "0.0.36"
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.36",
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.36"
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)
@@ -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.36",
31
+ user_agent: str = "sfq/0.0.38",
32
32
  sforce_client: str = "_auto",
33
33
  high_api_usage_threshold: int = 80,
34
34
  ) -> None:
@@ -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 escape(str(val))
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}">{escape(col)}</th>' for col in columns)
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>{escape(col)}</th>" for col in columns)
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
- 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:
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
- record.pop("attributes", None)
320
- normalized_records.append(record)
321
- return dicts_to_html_table(normalized_records, styled=styled)
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
@@ -3,5 +3,5 @@ requires-python = ">=3.9"
3
3
 
4
4
  [[package]]
5
5
  name = "sfq"
6
- version = "0.0.36"
6
+ version = "0.0.38"
7
7
  source = { editable = "." }
@@ -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