sfq 0.0.33__py3-none-any.whl → 0.0.34__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- sfq/__init__.py +18 -19
- sfq/crud.py +95 -27
- sfq/debug_cleanup.py +71 -0
- sfq/http_client.py +1 -1
- sfq/soap.py +12 -7
- {sfq-0.0.33.dist-info → sfq-0.0.34.dist-info}/METADATA +1 -1
- sfq-0.0.34.dist-info/RECORD +14 -0
- sfq-0.0.33.dist-info/RECORD +0 -13
- {sfq-0.0.33.dist-info → sfq-0.0.34.dist-info}/WHEEL +0 -0
sfq/__init__.py
CHANGED
@@ -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.
|
46
|
+
__version__ = "0.0.34"
|
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.
|
70
|
+
user_agent: str = "sfq/0.0.34",
|
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.
|
135
|
+
self.__version__ = "0.0.34"
|
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
|
534
|
-
|
535
|
-
|
536
|
-
|
537
|
-
|
538
|
-
|
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
|
-
|
551
|
-
|
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
|
"""
|
sfq/crud.py
CHANGED
@@ -37,23 +37,17 @@ class CRUDClient:
|
|
37
37
|
self.soap_client = soap_client
|
38
38
|
self.api_version = api_version
|
39
39
|
|
40
|
-
def
|
40
|
+
def _soap_batch_operation(
|
41
41
|
self,
|
42
42
|
sobject: str,
|
43
|
-
|
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
|
-
|
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(
|
74
|
-
|
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
|
-
|
78
|
-
for i in range(0, len(
|
72
|
+
data_list[i : i + batch_size]
|
73
|
+
for i in range(0, len(data_list), batch_size)
|
79
74
|
]
|
80
75
|
|
81
|
-
def
|
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=
|
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("
|
114
|
-
logger.trace("
|
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("
|
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
|
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(
|
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]]:
|
sfq/debug_cleanup.py
ADDED
@@ -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)
|
sfq/http_client.py
CHANGED
sfq/soap.py
CHANGED
@@ -100,13 +100,18 @@ class SOAPClient:
|
|
100
100
|
else:
|
101
101
|
records = data
|
102
102
|
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
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
|
@@ -0,0 +1,14 @@
|
|
1
|
+
sfq/__init__.py,sha256=OcSYzIjdE1cCm8wiGcK_HFIDFvrle_IHKo54EIeSpns,19929
|
2
|
+
sfq/_cometd.py,sha256=QqdSGsms9uFm7vgmxgau7m2UuLHztK1yjN-BNjeo8xM,10381
|
3
|
+
sfq/auth.py,sha256=bD7kEI5UpUAh0xpE2GzB7EatfLE0q-rqG7tOpqn_cQY,13985
|
4
|
+
sfq/crud.py,sha256=fj4wPMt0DcrMKbMWQ9AUMsUNUWicsY93LP_3Q7lhmDU,20300
|
5
|
+
sfq/debug_cleanup.py,sha256=e2_Hpigy3F7XsATOUXo8DZNmuEIL9SDD0tBlZIZeQLc,2638
|
6
|
+
sfq/exceptions.py,sha256=HZctvGj1SGguca0oG6fqSmf3KDbq4v68FfQfqB-crpo,906
|
7
|
+
sfq/http_client.py,sha256=02x9mT0v2EYgUQVLakvzjp0IZUrIqp1zuRw2H2k8Zo0,11636
|
8
|
+
sfq/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
9
|
+
sfq/query.py,sha256=AoagL8PMKUcpbPPTcHJPKhmUdDDPa0La4JLC0TUN_Yc,14586
|
10
|
+
sfq/soap.py,sha256=FM4msP9ErrgLFaNOQy_kYVde8QFkT4yQu9TfMiZG0VA,7006
|
11
|
+
sfq/utils.py,sha256=gx_pCZmOykYz19wwx6O2BTGj7bQfzhX_SuLHnYGCWuc,6234
|
12
|
+
sfq-0.0.34.dist-info/METADATA,sha256=LlV877Eud-Cg2CAQLJHVfmvg6-kWHlHASDYt5zAJg5s,6899
|
13
|
+
sfq-0.0.34.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
14
|
+
sfq-0.0.34.dist-info/RECORD,,
|
sfq-0.0.33.dist-info/RECORD
DELETED
@@ -1,13 +0,0 @@
|
|
1
|
-
sfq/__init__.py,sha256=kTT0wRF7zWpG-2KYwvPwDlruzx0SB9fqIxGXq4P51oE,20134
|
2
|
-
sfq/_cometd.py,sha256=QqdSGsms9uFm7vgmxgau7m2UuLHztK1yjN-BNjeo8xM,10381
|
3
|
-
sfq/auth.py,sha256=bD7kEI5UpUAh0xpE2GzB7EatfLE0q-rqG7tOpqn_cQY,13985
|
4
|
-
sfq/crud.py,sha256=oYx74HJN18fnsVVfEUIGb-H9YN0EAgH3HmjktN4GjuM,17829
|
5
|
-
sfq/exceptions.py,sha256=HZctvGj1SGguca0oG6fqSmf3KDbq4v68FfQfqB-crpo,906
|
6
|
-
sfq/http_client.py,sha256=JuPZ4YYR5y0Hhcn1HffMMcQcJPS_rht-1RewrTWn7bg,11636
|
7
|
-
sfq/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
8
|
-
sfq/query.py,sha256=AoagL8PMKUcpbPPTcHJPKhmUdDDPa0La4JLC0TUN_Yc,14586
|
9
|
-
sfq/soap.py,sha256=jQkC6D4z5iHFkDFOQhaR9Saj4sy3Qxn_zp0VPD-BmlQ,6728
|
10
|
-
sfq/utils.py,sha256=gx_pCZmOykYz19wwx6O2BTGj7bQfzhX_SuLHnYGCWuc,6234
|
11
|
-
sfq-0.0.33.dist-info/METADATA,sha256=qOEjgwIpgmx4J-ca_1knpMPVZU_zoBTqcO7ic_nWwIw,6899
|
12
|
-
sfq-0.0.33.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
13
|
-
sfq-0.0.33.dist-info/RECORD,,
|
File without changes
|