sfq 0.0.33__tar.gz → 0.0.35__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 (40) hide show
  1. {sfq-0.0.33 → sfq-0.0.35}/.github/workflows/publish.yml +3 -0
  2. {sfq-0.0.33 → sfq-0.0.35}/PKG-INFO +1 -1
  3. {sfq-0.0.33 → sfq-0.0.35}/pyproject.toml +1 -1
  4. {sfq-0.0.33 → sfq-0.0.35}/src/sfq/__init__.py +18 -19
  5. {sfq-0.0.33 → sfq-0.0.35}/src/sfq/crud.py +95 -27
  6. sfq-0.0.35/src/sfq/debug_cleanup.py +71 -0
  7. {sfq-0.0.33 → sfq-0.0.35}/src/sfq/http_client.py +1 -1
  8. {sfq-0.0.33 → sfq-0.0.35}/src/sfq/soap.py +12 -7
  9. {sfq-0.0.33 → sfq-0.0.35}/tests/test_cdelete.py +1 -1
  10. {sfq-0.0.33 → sfq-0.0.35}/tests/test_compatibility.py +0 -3
  11. {sfq-0.0.33 → sfq-0.0.35}/tests/test_cquery.py +1 -1
  12. {sfq-0.0.33 → sfq-0.0.35}/tests/test_create.py +1 -1
  13. {sfq-0.0.33 → sfq-0.0.35}/tests/test_crud_e2e.py +2 -2
  14. {sfq-0.0.33 → sfq-0.0.35}/tests/test_cupdate.py +1 -1
  15. sfq-0.0.33/tests/test_debug_cleanup.py → sfq-0.0.35/tests/test_debug_cleanup_e2e.py +46 -7
  16. sfq-0.0.35/tests/test_debug_cleanup_unit.py +57 -0
  17. {sfq-0.0.33 → sfq-0.0.35}/tests/test_limits_api.py +1 -1
  18. {sfq-0.0.33 → sfq-0.0.35}/tests/test_log_trace_redact.py +1 -1
  19. {sfq-0.0.33 → sfq-0.0.35}/tests/test_open_frontdoor.py +1 -1
  20. {sfq-0.0.33 → sfq-0.0.35}/tests/test_query.py +1 -1
  21. {sfq-0.0.33 → sfq-0.0.35}/tests/test_query_e2e.py +2 -2
  22. {sfq-0.0.33 → sfq-0.0.35}/tests/test_query_integration.py +2 -2
  23. sfq-0.0.35/tests/test_soap_batch_operation.py +42 -0
  24. {sfq-0.0.33 → sfq-0.0.35}/tests/test_static_resources.py +1 -1
  25. {sfq-0.0.33 → sfq-0.0.35}/uv.lock +1 -1
  26. {sfq-0.0.33 → sfq-0.0.35}/.gitignore +0 -0
  27. {sfq-0.0.33 → sfq-0.0.35}/.python-version +0 -0
  28. {sfq-0.0.33 → sfq-0.0.35}/README.md +0 -0
  29. {sfq-0.0.33 → sfq-0.0.35}/src/sfq/_cometd.py +0 -0
  30. {sfq-0.0.33 → sfq-0.0.35}/src/sfq/auth.py +0 -0
  31. {sfq-0.0.33 → sfq-0.0.35}/src/sfq/exceptions.py +0 -0
  32. {sfq-0.0.33 → sfq-0.0.35}/src/sfq/py.typed +0 -0
  33. {sfq-0.0.33 → sfq-0.0.35}/src/sfq/query.py +0 -0
  34. {sfq-0.0.33 → sfq-0.0.35}/src/sfq/utils.py +0 -0
  35. {sfq-0.0.33 → sfq-0.0.35}/tests/test_auth.py +0 -0
  36. {sfq-0.0.33 → sfq-0.0.35}/tests/test_crud.py +0 -0
  37. {sfq-0.0.33 → sfq-0.0.35}/tests/test_http_client.py +0 -0
  38. {sfq-0.0.33 → sfq-0.0.35}/tests/test_query_client.py +0 -0
  39. {sfq-0.0.33 → sfq-0.0.35}/tests/test_soap.py +0 -0
  40. {sfq-0.0.33 → sfq-0.0.35}/tests/test_utils.py +0 -0
@@ -82,6 +82,9 @@ jobs:
82
82
  echo "Updating src/sfq/__init__.py __version__ to $VERSION"
83
83
  sed -i -E "s/(self.__version__ = \")[0-9]+\.[0-9]+\.[0-9]+(\")/\1$VERSION\2/" src/sfq/__init__.py
84
84
  sed -i -E "s/(__version__ = \")[0-9]+\.[0-9]+\.[0-9]+(\")/\1$VERSION\2/" src/sfq/__init__.py
85
+
86
+ echo "Updating src/sfq/http_client.py user_agent to $VERSION"
87
+ sed -i -E "s/(user_agent: str = \"sfq\/)[0-9]+\.[0-9]+\.[0-9]+(\")/\1$VERSION\2/" src/sfq/http_client.py
85
88
 
86
89
  - name: Run tests
87
90
  run: pytest --verbose --strict-config
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sfq
3
- Version: 0.0.33
3
+ Version: 0.0.35
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.33"
3
+ version = "0.0.35"
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" }]
@@ -25,6 +25,7 @@ from .http_client import HTTPClient
25
25
  from .query import QueryClient
26
26
  from .soap import SOAPClient
27
27
  from .utils import get_logger
28
+ from .debug_cleanup import DebugCleanup
28
29
 
29
30
  # Define public API for documentation tools
30
31
  __all__ = [
@@ -42,7 +43,7 @@ __all__ = [
42
43
  "__version__",
43
44
  ]
44
45
 
45
- __version__ = "0.0.33"
46
+ __version__ = "0.0.35"
46
47
  """
47
48
  ### `__version__`
48
49
 
@@ -66,7 +67,7 @@ class SFAuth:
66
67
  access_token: Optional[str] = None,
67
68
  token_expiration_time: Optional[float] = None,
68
69
  token_lifetime: int = 15 * 60,
69
- user_agent: str = "sfq/0.0.33",
70
+ user_agent: str = "sfq/0.0.35",
70
71
  sforce_client: str = "_auto",
71
72
  proxy: str = "_auto",
72
73
  ) -> None:
@@ -127,8 +128,11 @@ class SFAuth:
127
128
  api_version=api_version,
128
129
  )
129
130
 
131
+ # Initialize the DebugCleanup
132
+ self._debug_cleanup = DebugCleanup(sf_auth=self)
133
+
130
134
  # Store version information
131
- self.__version__ = "0.0.33"
135
+ self.__version__ = "0.0.35"
132
136
  """
133
137
  ### `__version__`
134
138
 
@@ -530,25 +534,20 @@ class SFAuth:
530
534
  sobject, insert_list, batch_size, max_workers, api_type
531
535
  )
532
536
 
533
- def _debug_cleanup_apex_logs(self):
534
- """
535
- This function performs cleanup operations for Apex debug logs.
536
- """
537
- apex_logs = self.query("SELECT Id FROM ApexLog ORDER BY LogLength DESC")
538
- if apex_logs and apex_logs.get("records"):
539
- log_ids = [log["Id"] for log in apex_logs["records"]]
540
- if log_ids:
541
- delete_response = self.cdelete(log_ids)
542
- logger.debug("Deleted Apex logs: %s", delete_response)
543
- else:
544
- logger.debug("No Apex logs found to delete.")
545
-
546
- def debug_cleanup(self, apex_logs: bool = True) -> None:
537
+ def debug_cleanup(
538
+ self,
539
+ apex_logs: bool = True,
540
+ expired_apex_flags: bool = True,
541
+ all_apex_flags: bool = False,
542
+ ) -> None:
547
543
  """
548
544
  Perform cleanup operations for Apex debug logs.
549
545
  """
550
- if apex_logs:
551
- self._debug_cleanup_apex_logs()
546
+ self._debug_cleanup.debug_cleanup(
547
+ apex_logs=apex_logs,
548
+ expired_apex_flags=expired_apex_flags,
549
+ all_apex_flags=all_apex_flags,
550
+ )
552
551
 
553
552
  def open_frontdoor(self) -> None:
554
553
  """
@@ -37,23 +37,17 @@ class CRUDClient:
37
37
  self.soap_client = soap_client
38
38
  self.api_version = api_version
39
39
 
40
- def create(
40
+ def _soap_batch_operation(
41
41
  self,
42
42
  sobject: str,
43
- insert_list: List[Dict[str, Any]],
43
+ data_list,
44
+ method: str,
44
45
  batch_size: int = 200,
45
46
  max_workers: int = None,
46
47
  api_type: Literal["enterprise", "tooling"] = "enterprise",
47
48
  ) -> Optional[Dict[str, Any]]:
48
49
  """
49
- Execute the Insert API to insert multiple records via SOAP calls.
50
-
51
- :param sobject: The name of the sObject to insert into.
52
- :param insert_list: A list of dictionaries, each representing a record to insert.
53
- :param batch_size: The number of records to insert in each batch (default is 200).
54
- :param max_workers: The maximum number of threads to spawn for concurrent execution.
55
- :param api_type: API type to use ('enterprise' or 'tooling').
56
- :return: JSON response from the insert request or None on failure.
50
+ Internal helper for batch SOAP operations (create/delete).
57
51
  """
58
52
  endpoint = "/services/Soap/"
59
53
  if api_type == "enterprise":
@@ -67,21 +61,20 @@ class CRUDClient:
67
61
  )
68
62
  return None
69
63
 
70
- # Handle API versioning in the endpoint
71
64
  endpoint = endpoint.replace("/v", "/")
72
65
 
73
- if isinstance(insert_list, dict):
74
- insert_list = [insert_list]
66
+ if isinstance(data_list, dict) and method == "create":
67
+ data_list = [data_list]
68
+ if isinstance(data_list, str) and method == "delete":
69
+ data_list = [data_list]
75
70
 
76
71
  chunks = [
77
- insert_list[i : i + batch_size]
78
- for i in range(0, len(insert_list), batch_size)
72
+ data_list[i : i + batch_size]
73
+ for i in range(0, len(data_list), batch_size)
79
74
  ]
80
75
 
81
- def insert_chunk(chunk: List[Dict[str, Any]]) -> Optional[Dict[str, Any]]:
82
- """Insert a chunk of records using SOAP API."""
76
+ def process_chunk(chunk):
83
77
  try:
84
- # Get the access token for the SOAP header
85
78
  access_token = self.http_client.auth_manager.access_token
86
79
  if not access_token:
87
80
  logger.error("No access token available for SOAP request")
@@ -89,7 +82,7 @@ class CRUDClient:
89
82
 
90
83
  header = self.soap_client.generate_soap_header(access_token)
91
84
  body = self.soap_client.generate_soap_body(
92
- sobject=sobject, method="create", data=chunk
85
+ sobject=sobject, method=method, data=chunk
93
86
  )
94
87
  envelope = self.soap_client.generate_soap_envelope(
95
88
  header=header, body=body, api_type=api_type
@@ -99,8 +92,8 @@ class CRUDClient:
99
92
  soap_headers["Content-Type"] = "text/xml; charset=UTF-8"
100
93
  soap_headers["SOAPAction"] = '""'
101
94
 
102
- logger.trace("SOAP request envelope: %s", envelope)
103
- logger.trace("SOAP request headers: %s", soap_headers)
95
+ logger.trace(f"SOAP {method} request envelope: %s", envelope)
96
+ logger.trace(f"SOAP {method} request headers: %s", soap_headers)
104
97
 
105
98
  status_code, resp_data = self.http_client.send_request(
106
99
  method="POST",
@@ -109,25 +102,27 @@ class CRUDClient:
109
102
  body=envelope,
110
103
  )
111
104
 
105
+ logger.trace(f"SOAP {method} response status: {status_code}")
106
+ logger.trace(f"SOAP {method} raw response: {resp_data}")
107
+
112
108
  if status_code == 200:
113
- logger.debug("Insert API request successful.")
114
- logger.trace("Insert API response: %s", resp_data)
109
+ logger.debug(f"{method.capitalize()} API request successful.")
110
+ logger.trace(f"{method.capitalize()} API response: %s", resp_data)
115
111
  result = self.soap_client.extract_soap_result_fields(resp_data)
116
112
  if result:
117
113
  return result
118
114
  logger.error("Failed to extract fields from SOAP response.")
119
115
  else:
120
- logger.error("Insert API request failed: %s", status_code)
116
+ logger.error(f"{method.capitalize()} API request failed: %s", status_code)
121
117
  logger.debug("Response body: %s", resp_data)
122
118
  return None
123
-
124
119
  except Exception as e:
125
- logger.exception("Exception during insert chunk: %s", e)
120
+ logger.exception(f"Exception during {method} chunk: %s", e)
126
121
  return None
127
122
 
128
123
  results = []
129
124
  with ThreadPoolExecutor(max_workers=max_workers) as executor:
130
- futures = [executor.submit(insert_chunk, chunk) for chunk in chunks]
125
+ futures = [executor.submit(process_chunk, chunk) for chunk in chunks]
131
126
  for future in as_completed(futures):
132
127
  result = future.result()
133
128
  if result:
@@ -142,6 +137,79 @@ class CRUDClient:
142
137
 
143
138
  return combined_response or None
144
139
 
140
+ def create(
141
+ self,
142
+ sobject: str,
143
+ insert_list: List[Dict[str, Any]],
144
+ batch_size: int = 200,
145
+ max_workers: int = None,
146
+ api_type: Literal["enterprise", "tooling"] = "enterprise",
147
+ ) -> Optional[Dict[str, Any]]:
148
+ """
149
+ Execute the Insert API to insert multiple records via SOAP calls.
150
+ """
151
+ return self._soap_batch_operation(
152
+ sobject=sobject,
153
+ data_list=insert_list,
154
+ method="create",
155
+ batch_size=batch_size,
156
+ max_workers=max_workers,
157
+ api_type=api_type,
158
+ )
159
+
160
+ def update(
161
+ self,
162
+ sobject: str,
163
+ update_list: List[Dict[str, Any]],
164
+ batch_size: int = 200,
165
+ max_workers: int = None,
166
+ api_type: Literal["enterprise", "tooling"] = "enterprise",
167
+ ) -> Optional[Dict[str, Any]]:
168
+ """
169
+ Execute the Update API to update multiple records via SOAP calls.
170
+ :param sobject: The name of the sObject to update.
171
+ :param update_list: A list of dictionaries, each representing a record to update (must include Id).
172
+ :param batch_size: The number of records to update in each batch (default is 200).
173
+ :param max_workers: The maximum number of threads to spawn for concurrent execution.
174
+ :param api_type: API type to use ('enterprise' or 'tooling').
175
+ :return: JSON response from the update request or None on failure.
176
+ """
177
+ return self._soap_batch_operation(
178
+ sobject=sobject,
179
+ data_list=update_list,
180
+ method="update",
181
+ batch_size=batch_size,
182
+ max_workers=max_workers,
183
+ api_type=api_type,
184
+ )
185
+
186
+ def delete(
187
+ self,
188
+ sobject: str,
189
+ id_list: List[str],
190
+ batch_size: int = 200,
191
+ max_workers: int = None,
192
+ api_type: Literal["enterprise", "tooling"] = "enterprise",
193
+ ) -> Optional[Dict[str, Any]]:
194
+ """
195
+ Execute the Delete API to remove multiple records via SOAP calls.
196
+ :param sobject: The name of the sObject to delete from.
197
+ :param id_list: A list of record IDs to delete (strings, not dicts).
198
+ :param batch_size: The number of records to delete in each batch (default is 200).
199
+ :param max_workers: The maximum number of threads to spawn for concurrent execution.
200
+ :param api_type: API type to use ('enterprise' or 'tooling').
201
+ :return: JSON response from the delete request or None on failure.
202
+ """
203
+ # Pass list of IDs directly for SOAP delete
204
+ return self._soap_batch_operation(
205
+ sobject=sobject,
206
+ data_list=id_list,
207
+ method="delete",
208
+ batch_size=batch_size,
209
+ max_workers=max_workers,
210
+ api_type=api_type,
211
+ )
212
+
145
213
  def cupdate(
146
214
  self, update_dict: Dict[str, Any], batch_size: int = 25, max_workers: int = None
147
215
  ) -> Optional[Dict[str, Any]]:
@@ -0,0 +1,71 @@
1
+ """
2
+ Debug cleanup module for Salesforce-related debug artifacts.
3
+ """
4
+
5
+ from datetime import datetime, timezone
6
+ import logging
7
+ from typing import Dict, Any, Optional
8
+
9
+ logger = logging.getLogger("sfq")
10
+
11
+
12
+ class DebugCleanup:
13
+ """
14
+ Class handling debug artifact cleanup operations in Salesforce.
15
+ """
16
+
17
+ def __init__(self, sf_auth):
18
+ """
19
+ Initialize the DebugCleanup with an SFAuth instance.
20
+
21
+ :param sf_auth: An authenticated SFAuth instance
22
+ """
23
+ self._sf_auth = sf_auth
24
+
25
+ def _debug_cleanup_apex_logs(self) -> None:
26
+ """
27
+ This function performs cleanup operations for Apex debug logs.
28
+ """
29
+ apex_logs = self._sf_auth.query("SELECT Id FROM ApexLog ORDER BY LogLength DESC")
30
+ if apex_logs and apex_logs.get("records"):
31
+ log_ids = [log["Id"] for log in apex_logs["records"]]
32
+ if log_ids:
33
+ delete_response = self._sf_auth.cdelete(log_ids)
34
+ logger.debug("Deleted Apex logs: %s", delete_response)
35
+ else:
36
+ logger.debug("No Apex logs found to delete.")
37
+
38
+ def _debug_cleanup_trace_flags(self, expired_only: bool) -> None:
39
+ """
40
+ This function performs cleanup operations for Trace Flags.
41
+ """
42
+ if expired_only:
43
+ now_iso_format = datetime.now(timezone.utc).strftime('%Y-%m-%dT%H:%M:%SZ')
44
+ query = f"SELECT Id, ExpirationDate FROM TraceFlag WHERE ExpirationDate < {now_iso_format}"
45
+ else:
46
+ query = "SELECT Id, ExpirationDate FROM TraceFlag"
47
+
48
+ traceflags = self._sf_auth.tooling_query(query)
49
+ if traceflags['totalSize'] == 0:
50
+ logger.debug("No expired Trace Flag configurations to delete.")
51
+ return
52
+
53
+ trace_flag_ids = [tf["Id"] for tf in traceflags["records"]]
54
+ logger.debug("Deleting Trace Flags: %s", trace_flag_ids)
55
+ results = self._sf_auth._crud_client.delete("TraceFlag", trace_flag_ids, api_type="tooling")
56
+ # results = self._sf_auth.delete("TraceFlag", trace_flag_ids)
57
+ logger.debug("Deleted Trace Flags: %s", results)
58
+
59
+ def debug_cleanup(self, apex_logs: bool = True, expired_apex_flags: bool = True, all_apex_flags: bool = False) -> None:
60
+ """
61
+ Perform cleanup operations for Apex debug logs.
62
+
63
+ :param apex_logs: Whether to clean up Apex logs (default: True)
64
+ """
65
+ if apex_logs:
66
+ self._debug_cleanup_apex_logs()
67
+
68
+ if all_apex_flags:
69
+ self._debug_cleanup_trace_flags(expired_only=False)
70
+ elif expired_apex_flags:
71
+ self._debug_cleanup_trace_flags(expired_only=True)
@@ -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.33",
31
+ user_agent: str = "sfq/0.0.35",
32
32
  sforce_client: str = "_auto",
33
33
  high_api_usage_threshold: int = 80,
34
34
  ) -> None:
@@ -100,13 +100,18 @@ class SOAPClient:
100
100
  else:
101
101
  records = data
102
102
 
103
- sobjects = "".join(
104
- f'<sObjects xsi:type="{sobject}">'
105
- + "".join(f"<{k}>{v}</{k}>" for k, v in record.items())
106
- + "</sObjects>"
107
- for record in records
108
- )
109
- return f"<soapenv:Body><{method}>{sobjects}</{method}></soapenv:Body>"
103
+ if method == "delete":
104
+ # For delete, records is a list of IDs (strings)
105
+ id_tags = "".join(f"<ID>{id}</ID>" for id in records)
106
+ return f"<soapenv:Body><delete>{id_tags}</delete></soapenv:Body>"
107
+ else:
108
+ sobjects = "".join(
109
+ f'<sObjects xsi:type="{sobject}">'
110
+ + "".join(f"<{k}>{v}</{k}>" for k, v in record.items())
111
+ + "</sObjects>"
112
+ for record in records
113
+ )
114
+ return f"<soapenv:Body><{method}>{sobjects}</{method}></soapenv:Body>"
110
115
 
111
116
  def extract_soap_result_fields(
112
117
  self, xml_string: str
@@ -21,7 +21,7 @@ def sf_instance():
21
21
  sf = SFAuth(
22
22
  instance_url=os.getenv("SF_INSTANCE_URL"),
23
23
  client_id=os.getenv("SF_CLIENT_ID"),
24
- client_secret=os.getenv("SF_CLIENT_SECRET"),
24
+ client_secret=os.getenv("SF_CLIENT_SECRET").strip(),
25
25
  refresh_token=os.getenv("SF_REFRESH_TOKEN"),
26
26
  )
27
27
  return sf
@@ -541,9 +541,6 @@ class TestPrivateMethodCompatibility:
541
541
  assert hasattr(mock_sf_auth, "debug_cleanup")
542
542
  assert callable(mock_sf_auth.debug_cleanup)
543
543
 
544
- assert hasattr(mock_sf_auth, "_debug_cleanup_apex_logs")
545
- assert callable(mock_sf_auth._debug_cleanup_apex_logs)
546
-
547
544
  def test_frontdoor_method_available(self, mock_sf_auth):
548
545
  """Test that open_frontdoor method is available."""
549
546
  assert hasattr(mock_sf_auth, "open_frontdoor")
@@ -21,7 +21,7 @@ def sf_instance():
21
21
  sf = SFAuth(
22
22
  instance_url=os.getenv("SF_INSTANCE_URL"),
23
23
  client_id=os.getenv("SF_CLIENT_ID"),
24
- client_secret=os.getenv("SF_CLIENT_SECRET"),
24
+ client_secret=os.getenv("SF_CLIENT_SECRET").strip(),
25
25
  refresh_token=os.getenv("SF_REFRESH_TOKEN"),
26
26
  )
27
27
  return sf
@@ -21,7 +21,7 @@ def sf_instance():
21
21
  sf = SFAuth(
22
22
  instance_url=os.getenv("SF_INSTANCE_URL"),
23
23
  client_id=os.getenv("SF_CLIENT_ID"),
24
- client_secret=os.getenv("SF_CLIENT_SECRET"),
24
+ client_secret=os.getenv("SF_CLIENT_SECRET").strip(),
25
25
  refresh_token=os.getenv("SF_REFRESH_TOKEN"),
26
26
  )
27
27
  return sf
@@ -34,7 +34,7 @@ def auth_manager():
34
34
  return AuthManager(
35
35
  instance_url=os.getenv("SF_INSTANCE_URL"),
36
36
  client_id=os.getenv("SF_CLIENT_ID"),
37
- client_secret=os.getenv("SF_CLIENT_SECRET"),
37
+ client_secret=os.getenv("SF_CLIENT_SECRET").strip(),
38
38
  refresh_token=os.getenv("SF_REFRESH_TOKEN"),
39
39
  )
40
40
 
@@ -74,7 +74,7 @@ def sf_auth():
74
74
  return SFAuth(
75
75
  instance_url=os.getenv("SF_INSTANCE_URL"),
76
76
  client_id=os.getenv("SF_CLIENT_ID"),
77
- client_secret=os.getenv("SF_CLIENT_SECRET"),
77
+ client_secret=os.getenv("SF_CLIENT_SECRET").strip(),
78
78
  refresh_token=os.getenv("SF_REFRESH_TOKEN"),
79
79
  )
80
80
 
@@ -23,7 +23,7 @@ def sf_instance():
23
23
  sf = SFAuth(
24
24
  instance_url=os.getenv("SF_INSTANCE_URL"),
25
25
  client_id=os.getenv("SF_CLIENT_ID"),
26
- client_secret=os.getenv("SF_CLIENT_SECRET"),
26
+ client_secret=os.getenv("SF_CLIENT_SECRET").strip(),
27
27
  refresh_token=os.getenv("SF_REFRESH_TOKEN"),
28
28
  )
29
29
  return sf
@@ -26,12 +26,54 @@ def sf_instance():
26
26
  sf = SFAuth(
27
27
  instance_url=os.getenv("SF_INSTANCE_URL"),
28
28
  client_id=os.getenv("SF_CLIENT_ID"),
29
- client_secret=os.getenv("SF_CLIENT_SECRET"),
29
+ client_secret=os.getenv("SF_CLIENT_SECRET").strip(),
30
30
  refresh_token=os.getenv("SF_REFRESH_TOKEN"),
31
31
  )
32
32
  return sf
33
33
 
34
34
 
35
+ def test_trace_flag_expired_deletions(sf_instance):
36
+ """
37
+ This test case ensures that unused TraceFlag's in the Tooling API are deleted when called.
38
+ """
39
+ now_iso_format = datetime.now(timezone.utc).strftime('%Y-%m-%dT%H:%M:%SZ')
40
+
41
+ query = f"SELECT Id, ExpirationDate FROM TraceFlag WHERE ExpirationDate < {now_iso_format}"
42
+
43
+ traceflags = sf_instance.tooling_query(query)
44
+ if traceflags['totalSize'] == 0:
45
+ pytest.skip("No expired Trace Flag configurations to delete.")
46
+
47
+ initial_count = len(traceflags["records"])
48
+ assert initial_count > 0, "Initial count of TraceFlags should be greater than zero."
49
+
50
+ results = sf_instance.debug_cleanup(apex_logs=False, expired_apex_flags=True, all_apex_flags=False)
51
+ assert results is None, "Expected no return value from debug_cleanup."
52
+
53
+ traceflags_after = sf_instance.tooling_query(query)
54
+ assert len(traceflags_after["records"]) == 0, "TraceFlags were not deleted successfully."
55
+
56
+
57
+ def test_trace_flag_all_deletions(sf_instance):
58
+ """
59
+ This test case ensures that unused TraceFlag's in the Tooling API are deleted when called.
60
+ """
61
+ query = f"SELECT Id, ExpirationDate FROM TraceFlag"
62
+
63
+ traceflags = sf_instance.tooling_query(query)
64
+ if traceflags['totalSize'] == 0:
65
+ pytest.skip("No expired Trace Flag configurations to delete.")
66
+
67
+ initial_count = len(traceflags["records"])
68
+ assert initial_count > 0, "Initial count of TraceFlags should be greater than zero."
69
+
70
+ results = sf_instance.debug_cleanup(apex_logs=False, expired_apex_flags=False, all_apex_flags=True)
71
+ assert results is None, "Expected no return value from debug_cleanup."
72
+
73
+
74
+ traceflags_after = sf_instance.tooling_query(query)
75
+ assert len(traceflags_after["records"]) == 0, "TraceFlags were not deleted successfully."
76
+
35
77
  def test_debug_cleanup(sf_instance, already_executed: bool = False):
36
78
  """
37
79
  Test the debug_cleanup method of SFAuth.
@@ -40,7 +82,7 @@ def test_debug_cleanup(sf_instance, already_executed: bool = False):
40
82
  # Check if any Apex logs already exist
41
83
  apex_logs = sf_instance.query("SELECT Id FROM ApexLog LIMIT 1")
42
84
  if apex_logs.get("records"):
43
- sf_instance.debug_cleanup(apex_logs=True)
85
+ sf_instance.debug_cleanup(apex_logs=True, expired_apex_flags=False, all_apex_flags=False)
44
86
  apex_logs_after = sf_instance.query("SELECT Id FROM ApexLog LIMIT 1")
45
87
  assert len(apex_logs_after.get("records", [])) == 0, (
46
88
  "Apex logs were not cleaned up successfully."
@@ -74,13 +116,9 @@ def test_debug_cleanup(sf_instance, already_executed: bool = False):
74
116
  datetime.now(timezone.utc) + timedelta(minutes=5)
75
117
  ).isoformat(),
76
118
  }
77
- resp = sf_instance._create(
119
+ _ = sf_instance._create(
78
120
  sobject="TraceFlag", insert_list=[traceflag_payload], api_type="tooling"
79
121
  )
80
- with open("debug_payload.json", "w") as f:
81
- f.write(json.dumps(resp, indent=2))
82
- with open("debug_payload.json", "w") as f:
83
- f.write(json.dumps(traceflag_payload, indent=2))
84
122
 
85
123
  traceflag_query = sf_instance.tooling_query(
86
124
  f"SELECT Id FROM TraceFlag WHERE TracedEntityId = '{sf_instance.user_id}' LIMIT 1"
@@ -140,3 +178,4 @@ def test_debug_cleanup(sf_instance, already_executed: bool = False):
140
178
 
141
179
  sleep(1) # Race condition mitigation
142
180
  return test_debug_cleanup(sf_instance, already_executed=True)
181
+
@@ -0,0 +1,57 @@
1
+ import pytest
2
+ from unittest.mock import MagicMock
3
+ from sfq.debug_cleanup import DebugCleanup
4
+
5
+ class DummySFAuth:
6
+ def __init__(self):
7
+ self._crud_client = MagicMock()
8
+ self.cdelete = MagicMock()
9
+ self.query = MagicMock()
10
+ self.tooling_query = MagicMock()
11
+
12
+ @pytest.fixture
13
+ def dummy_sf_auth():
14
+ return DummySFAuth()
15
+
16
+ @pytest.fixture
17
+ def debug_cleanup(dummy_sf_auth):
18
+ return DebugCleanup(dummy_sf_auth)
19
+
20
+ def test_debug_cleanup_apex_logs(debug_cleanup, dummy_sf_auth):
21
+ dummy_sf_auth.query.return_value = {"records": [{"Id": "log1"}, {"Id": "log2"}]}
22
+ debug_cleanup._debug_cleanup_apex_logs()
23
+ dummy_sf_auth.cdelete.assert_called_once_with(["log1", "log2"])
24
+
25
+ def test_debug_cleanup_apex_logs_no_logs(debug_cleanup, dummy_sf_auth):
26
+ dummy_sf_auth.query.return_value = {"records": []}
27
+ debug_cleanup._debug_cleanup_apex_logs()
28
+ dummy_sf_auth.cdelete.assert_not_called()
29
+
30
+ def test_debug_cleanup_trace_flags_expired(debug_cleanup, dummy_sf_auth):
31
+ dummy_sf_auth.tooling_query.return_value = {"totalSize": 2, "records": [{"Id": "tf1"}, {"Id": "tf2"}]}
32
+ debug_cleanup._sf_auth._crud_client.delete.return_value = "deleted"
33
+ debug_cleanup._debug_cleanup_trace_flags(expired_only=True)
34
+ dummy_sf_auth._crud_client.delete.assert_called_once_with("TraceFlag", ["tf1", "tf2"], api_type="tooling")
35
+
36
+ def test_debug_cleanup_trace_flags_none(debug_cleanup, dummy_sf_auth):
37
+ dummy_sf_auth.tooling_query.return_value = {"totalSize": 0, "records": []}
38
+ debug_cleanup._debug_cleanup_trace_flags(expired_only=True)
39
+ dummy_sf_auth._crud_client.delete.assert_not_called()
40
+
41
+ def test_debug_cleanup_dispatch(debug_cleanup, dummy_sf_auth):
42
+ # Test all_apex_flags True
43
+ dummy_sf_auth.tooling_query.return_value = {"totalSize": 1, "records": [{"Id": "tf1"}]}
44
+ debug_cleanup._sf_auth._crud_client.delete.reset_mock()
45
+ debug_cleanup.debug_cleanup(apex_logs=False, expired_apex_flags=True, all_apex_flags=True)
46
+ dummy_sf_auth._crud_client.delete.assert_called_once_with("TraceFlag", ["tf1"], api_type="tooling")
47
+
48
+ # Test expired_apex_flags True, all_apex_flags False
49
+ debug_cleanup._sf_auth._crud_client.delete.reset_mock()
50
+ debug_cleanup.debug_cleanup(apex_logs=False, expired_apex_flags=True, all_apex_flags=False)
51
+ dummy_sf_auth._crud_client.delete.assert_called_once_with("TraceFlag", ["tf1"], api_type="tooling")
52
+
53
+ # Test apex_logs True
54
+ dummy_sf_auth.query.return_value = {"records": [{"Id": "log1"}]}
55
+ debug_cleanup._sf_auth.cdelete.reset_mock()
56
+ debug_cleanup.debug_cleanup(apex_logs=True, expired_apex_flags=False, all_apex_flags=False)
57
+ dummy_sf_auth.cdelete.assert_called_once_with(["log1"])
@@ -21,7 +21,7 @@ def sf_instance():
21
21
  sf = SFAuth(
22
22
  instance_url=os.getenv("SF_INSTANCE_URL"),
23
23
  client_id=os.getenv("SF_CLIENT_ID"),
24
- client_secret=os.getenv("SF_CLIENT_SECRET"),
24
+ client_secret=os.getenv("SF_CLIENT_SECRET").strip(),
25
25
  refresh_token=os.getenv("SF_REFRESH_TOKEN"),
26
26
  )
27
27
  return sf
@@ -23,7 +23,7 @@ def sf_instance():
23
23
  sf = SFAuth(
24
24
  instance_url=os.getenv("SF_INSTANCE_URL"),
25
25
  client_id=os.getenv("SF_CLIENT_ID"),
26
- client_secret=os.getenv("SF_CLIENT_SECRET"),
26
+ client_secret=os.getenv("SF_CLIENT_SECRET").strip(),
27
27
  refresh_token=os.getenv("SF_REFRESH_TOKEN"),
28
28
  )
29
29
  return sf
@@ -25,7 +25,7 @@ def sf_instance():
25
25
  return SFAuth(
26
26
  instance_url=os.getenv("SF_INSTANCE_URL"),
27
27
  client_id=os.getenv("SF_CLIENT_ID"),
28
- client_secret=os.getenv("SF_CLIENT_SECRET"),
28
+ client_secret=os.getenv("SF_CLIENT_SECRET").strip(),
29
29
  refresh_token=os.getenv("SF_REFRESH_TOKEN"),
30
30
  )
31
31
 
@@ -21,7 +21,7 @@ def sf_instance():
21
21
  sf = SFAuth(
22
22
  instance_url=os.getenv("SF_INSTANCE_URL"),
23
23
  client_id=os.getenv("SF_CLIENT_ID"),
24
- client_secret=os.getenv("SF_CLIENT_SECRET"),
24
+ client_secret=os.getenv("SF_CLIENT_SECRET").strip(),
25
25
  refresh_token=os.getenv("SF_REFRESH_TOKEN"),
26
26
  )
27
27
  return sf
@@ -32,7 +32,7 @@ def auth_manager():
32
32
  return AuthManager(
33
33
  instance_url=os.getenv("SF_INSTANCE_URL"),
34
34
  client_id=os.getenv("SF_CLIENT_ID"),
35
- client_secret=os.getenv("SF_CLIENT_SECRET"),
35
+ client_secret=os.getenv("SF_CLIENT_SECRET").strip(),
36
36
  refresh_token=os.getenv("SF_REFRESH_TOKEN"),
37
37
  )
38
38
 
@@ -52,7 +52,7 @@ def sf_instance():
52
52
  sf = SFAuth(
53
53
  instance_url=os.getenv("SF_INSTANCE_URL"),
54
54
  client_id=os.getenv("SF_CLIENT_ID"),
55
- client_secret=os.getenv("SF_CLIENT_SECRET"),
55
+ client_secret=os.getenv("SF_CLIENT_SECRET").strip(),
56
56
  refresh_token=os.getenv("SF_REFRESH_TOKEN"),
57
57
  )
58
58
  return sf
@@ -33,7 +33,7 @@ def sf_auth():
33
33
  return SFAuth(
34
34
  instance_url=os.getenv("SF_INSTANCE_URL"),
35
35
  client_id=os.getenv("SF_CLIENT_ID"),
36
- client_secret=os.getenv("SF_CLIENT_SECRET"),
36
+ client_secret=os.getenv("SF_CLIENT_SECRET").strip(),
37
37
  refresh_token=os.getenv("SF_REFRESH_TOKEN"),
38
38
  )
39
39
 
@@ -55,7 +55,7 @@ def modular_components():
55
55
  auth_manager = AuthManager(
56
56
  instance_url=os.getenv("SF_INSTANCE_URL"),
57
57
  client_id=os.getenv("SF_CLIENT_ID"),
58
- client_secret=os.getenv("SF_CLIENT_SECRET"),
58
+ client_secret=os.getenv("SF_CLIENT_SECRET").strip(),
59
59
  refresh_token=os.getenv("SF_REFRESH_TOKEN"),
60
60
  )
61
61
 
@@ -0,0 +1,42 @@
1
+ import pytest
2
+ from sfq.crud import CRUDClient
3
+ from sfq.soap import SOAPClient
4
+
5
+ class DummyHTTPClient:
6
+ def __init__(self):
7
+ self.auth_manager = type('Auth', (), {'access_token': 'dummy_token'})()
8
+ def get_common_headers(self):
9
+ return {}
10
+ def send_request(self, method, endpoint, headers, body):
11
+ # Simulate a successful SOAP response for create/delete/update
12
+ if '<create>' in body:
13
+ return 200, '<result><id>001xx000003DGbEAAW</id><success>true</success></result>'
14
+ elif '<delete>' in body:
15
+ return 200, '<result><id>001xx000003DGbEAAW</id><success>true</success></result>'
16
+ elif '<update>' in body:
17
+ return 200, '<result><id>001xx000003DGbEAAW</id><success>true</success></result>'
18
+ return 500, '<faultstring>Error</faultstring>'
19
+
20
+ @pytest.fixture
21
+ def crud_client():
22
+ http_client = DummyHTTPClient()
23
+ soap_client = SOAPClient(http_client)
24
+ return CRUDClient(http_client, soap_client)
25
+
26
+ def test_soap_batch_create(crud_client):
27
+ data = [{"Name": "Test Account"}]
28
+ result = crud_client._soap_batch_operation("Account", data, "create")
29
+ assert result is not None
30
+ assert result[0]["success"] == "true"
31
+
32
+ def test_soap_batch_delete(crud_client):
33
+ ids = ["001xx000003DGbEAAW"]
34
+ result = crud_client._soap_batch_operation("Account", ids, "delete")
35
+ assert result is not None
36
+ assert result[0]["success"] == "true"
37
+
38
+ def test_soap_batch_update(crud_client):
39
+ data = [{"Id": "001xx000003DGbEAAW", "Name": "Updated Account"}]
40
+ result = crud_client._soap_batch_operation("Account", data, "update")
41
+ assert result is not None
42
+ assert result[0]["success"] == "true"
@@ -22,7 +22,7 @@ def sf_instance():
22
22
  sf = SFAuth(
23
23
  instance_url=os.getenv("SF_INSTANCE_URL"),
24
24
  client_id=os.getenv("SF_CLIENT_ID"),
25
- client_secret=os.getenv("SF_CLIENT_SECRET"),
25
+ client_secret=os.getenv("SF_CLIENT_SECRET").strip(),
26
26
  refresh_token=os.getenv("SF_REFRESH_TOKEN"),
27
27
  )
28
28
  return sf
@@ -3,5 +3,5 @@ requires-python = ">=3.9"
3
3
 
4
4
  [[package]]
5
5
  name = "sfq"
6
- version = "0.0.33"
6
+ version = "0.0.35"
7
7
  source = { editable = "." }
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