sfq 0.0.28__tar.gz → 0.0.30__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sfq
3
- Version: 0.0.28
3
+ Version: 0.0.30
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.28"
3
+ version = "0.0.30"
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" }]
@@ -99,7 +99,7 @@ class SFAuth:
99
99
  access_token: Optional[str] = None,
100
100
  token_expiration_time: Optional[float] = None,
101
101
  token_lifetime: int = 15 * 60,
102
- user_agent: str = "sfq/0.0.28",
102
+ user_agent: str = "sfq/0.0.30",
103
103
  sforce_client: str = "_auto",
104
104
  proxy: str = "_auto",
105
105
  ) -> None:
@@ -1099,6 +1099,7 @@ class SFAuth:
1099
1099
  insert_list: List[Dict[str, Any]],
1100
1100
  batch_size: int = 200,
1101
1101
  max_workers: int = None,
1102
+ api_type: Literal["enterprise", "tooling", "metadata"] = "enterprise",
1102
1103
  ) -> Optional[Dict[str, Any]]:
1103
1104
  """
1104
1105
  Execute the Insert API to insert multiple records via SOAP calls.
@@ -1110,7 +1111,19 @@ class SFAuth:
1110
1111
  :return: JSON response from the insert request or None on failure.
1111
1112
  """
1112
1113
 
1113
- endpoint = f"/services/Soap/c/{self.api_version}"
1114
+ endpoint = "/services/Soap/"
1115
+ if api_type == "enterprise":
1116
+ endpoint += f"c/{self.api_version}"
1117
+ elif api_type == "tooling":
1118
+ endpoint += f"T/{self.api_version}"
1119
+ elif api_type == "metadata":
1120
+ endpoint += f"m/{self.api_version}"
1121
+ else:
1122
+ logger.error(
1123
+ "Invalid API type: %s. Must be one of: 'enterprise', 'tooling', 'metadata'.",
1124
+ api_type,
1125
+ )
1126
+ return None
1114
1127
 
1115
1128
  if isinstance(insert_list, dict):
1116
1129
  insert_list = [insert_list]
@@ -1165,3 +1178,23 @@ class SFAuth:
1165
1178
  ]
1166
1179
 
1167
1180
  return combined_response or None
1181
+
1182
+ def _debug_cleanup_apex_logs(self):
1183
+ """
1184
+ This function performs cleanup operations for Apex debug logs.
1185
+ """
1186
+ apex_logs = self.query("SELECT Id FROM ApexLog ORDER BY LogLength DESC")
1187
+ if apex_logs and apex_logs.get("records"):
1188
+ log_ids = [log["Id"] for log in apex_logs["records"]]
1189
+ if log_ids:
1190
+ delete_response = self.cdelete(log_ids)
1191
+ logger.debug("Deleted Apex logs: %s", delete_response)
1192
+ else:
1193
+ logger.debug("No Apex logs found to delete.")
1194
+
1195
+ def debug_cleanup(self, apex_logs: bool = True) -> None:
1196
+ """
1197
+ Perform cleanup operations for Apex debug logs.
1198
+ """
1199
+ if apex_logs:
1200
+ self._debug_cleanup_apex_logs()
@@ -0,0 +1,112 @@
1
+ import random
2
+ import os
3
+ import sys
4
+ from datetime import datetime
5
+ from pathlib import Path
6
+
7
+ import pytest
8
+
9
+ # --- Setup local import path ---
10
+ project_root = Path(__file__).resolve().parents[1]
11
+ src_path = project_root / "src"
12
+ sys.path.insert(0, str(src_path))
13
+ from sfq import SFAuth # noqa: E402
14
+
15
+
16
+ @pytest.fixture(scope="module")
17
+ def sf_instance():
18
+ required_env_vars = [
19
+ "SF_INSTANCE_URL",
20
+ "SF_CLIENT_ID",
21
+ "SF_CLIENT_SECRET",
22
+ "SF_REFRESH_TOKEN",
23
+ ]
24
+
25
+ missing_vars = [var for var in required_env_vars if not os.getenv(var)]
26
+ if missing_vars:
27
+ pytest.fail(f"Missing required env vars: {', '.join(missing_vars)}")
28
+
29
+ sf = SFAuth(
30
+ instance_url=os.getenv("SF_INSTANCE_URL"),
31
+ client_id=os.getenv("SF_CLIENT_ID"),
32
+ client_secret=os.getenv("SF_CLIENT_SECRET"),
33
+ refresh_token=os.getenv("SF_REFRESH_TOKEN"),
34
+ )
35
+ return sf
36
+
37
+
38
+ def test_simple_cupdate(sf_instance):
39
+ """Ensure that a simple update returns the expected results."""
40
+ query = "SELECT Id, CommentBody FROM FeedComment LIMIT 1"
41
+ response = sf_instance.query(query)
42
+ assert response and response["records"], "No FeedComment found for test."
43
+
44
+ update_dict = dict()
45
+ feed_comment_id = response["records"][0]["Id"]
46
+ update_dict[feed_comment_id] = {"CommentBody": f"Evaluated at {datetime.now()}"}
47
+
48
+ result = sf_instance._cupdate(update_dict)
49
+
50
+ # Validate the result
51
+ assert result, "CUpdate did not return a result."
52
+ assert isinstance(result, list), "CUpdate result is not a list."
53
+ assert len(result) == 1, "CUpdate result should contain one response."
54
+ assert "compositeResponse" in result[0], "CUpdate response does not contain 'compositeResponse'."
55
+ assert len(result[0]["compositeResponse"]) == 1, "CUpdate response should contain one composite response."
56
+ assert result[0]["compositeResponse"][0]["httpStatusCode"] == 204, "CUpdate did not return HTTP status 204."
57
+ assert result[0]["compositeResponse"][0]["body"] is None, "CUpdate response body should be None."
58
+
59
+ # Verify that the update was successful by querying the updated record
60
+ new_query = f"SELECT Id, CommentBody FROM FeedComment WHERE Id = '{feed_comment_id}'"
61
+ updated_response = sf_instance.query(new_query)
62
+ assert updated_response and updated_response["records"], "No updated FeedComment found."
63
+ assert len(updated_response["records"]) == 1, "Expected one updated FeedComment."
64
+ assert updated_response["records"][0]["CommentBody"] == update_dict[feed_comment_id]["CommentBody"], "Updated CommentBody does not match."
65
+
66
+ def test_cupdate_with_invalid_data(sf_instance):
67
+ """Test cupdate with invalid data to ensure it raises an error."""
68
+ update_dict = {"invalid_id": {"CommentBody": "This should fail"}}
69
+
70
+ res = sf_instance._cupdate(update_dict)
71
+ assert res, "CUpdate did not return a result."
72
+ assert isinstance(res, list), "CUpdate result is not a list."
73
+ assert len(res) == 1, "CUpdate result should contain one response."
74
+ assert "compositeResponse" in res[0], "CUpdate response does not contain 'compositeResponse'."
75
+ assert len(res[0]["compositeResponse"]) == 1, "CUpdate response should contain one composite response."
76
+ assert res[0]["compositeResponse"][0]["httpStatusCode"] == 404, "CUpdate did not return HTTP status 404."
77
+ assert res[0]["compositeResponse"][0]["body"][0]["errorCode"] == "NOT_FOUND", "Expected NOT_FOUND error code."
78
+ assert res[0]["compositeResponse"][0]["body"][0]["message"] == "The requested resource does not exist", "Expected NOT_FOUND error message."
79
+
80
+ def test_cupdate_pagination(sf_instance):
81
+ """Test cupdate with pagination to ensure it handles multiple records."""
82
+ size = 35 # using `/services/data/{self.api_version}/sobjects/{sobject}/{key}` which is 25 per pagination; I want to migrate to Soap for larger 200 batches.
83
+ query = f"SELECT Id, CommentBody FROM FeedComment LIMIT {size}"
84
+ initial_query_response = sf_instance.query(query)
85
+
86
+ response_count = len(initial_query_response["records"])
87
+ assert response_count == size, f"Expected {size} records, got {response_count}."
88
+ ids_updated = {record["Id"] for record in initial_query_response["records"]}
89
+
90
+ update_dict = dict()
91
+ for record in initial_query_response["records"]:
92
+ feed_comment_id = record["Id"]
93
+ update_dict[feed_comment_id] = {"CommentBody": f"sfq/{datetime.now().timestamp()} #{random.randint(1000, 9999)}"}
94
+
95
+ update_response = sf_instance._cupdate(update_dict)
96
+ assert update_response, "CUpdate did not return a result."
97
+ assert isinstance(update_response, list), "CUpdate result is not a list."
98
+
99
+ subset_ids = random.sample(list(ids_updated), min(50, len(ids_updated)))
100
+ subset_id_str = ", ".join([f"'{id_}'" for id_ in subset_ids])
101
+ subset_query = f"SELECT Id, CommentBody FROM FeedComment WHERE Id IN ({subset_id_str})"
102
+ updated_response = sf_instance.query(subset_query)
103
+
104
+ # Validate the updated records
105
+ assert updated_response, "Query for updated records did not return a result."
106
+ assert updated_response and updated_response["records"], "No updated FeedComment found."
107
+ assert len(updated_response["records"]) == len(subset_ids), "Expected updated records count does not match subset size."
108
+
109
+ # validate that all updated records have the expected CommentBody
110
+ for record in updated_response["records"]:
111
+ expected_comment_body = update_dict[record["Id"]]["CommentBody"]
112
+ assert record["CommentBody"] == expected_comment_body, f"Updated CommentBody for {record['Id']} does not match expected value."
@@ -0,0 +1,67 @@
1
+ import os
2
+ import sys
3
+ from pathlib import Path
4
+ from datetime import datetime, timedelta
5
+
6
+ import pytest
7
+
8
+ # --- Setup local import path ---
9
+ project_root = Path(__file__).resolve().parents[1]
10
+ src_path = project_root / "src"
11
+ sys.path.insert(0, str(src_path))
12
+ from sfq import SFAuth # noqa: E402
13
+
14
+
15
+ @pytest.fixture(scope="module")
16
+ def sf_instance():
17
+ required_env_vars = [
18
+ "SF_INSTANCE_URL",
19
+ "SF_CLIENT_ID",
20
+ "SF_CLIENT_SECRET",
21
+ "SF_REFRESH_TOKEN",
22
+ ]
23
+
24
+ missing_vars = [var for var in required_env_vars if not os.getenv(var)]
25
+ if missing_vars:
26
+ pytest.fail(f"Missing required env vars: {', '.join(missing_vars)}")
27
+
28
+ sf = SFAuth(
29
+ instance_url=os.getenv("SF_INSTANCE_URL"),
30
+ client_id=os.getenv("SF_CLIENT_ID"),
31
+ client_secret=os.getenv("SF_CLIENT_SECRET"),
32
+ refresh_token=os.getenv("SF_REFRESH_TOKEN"),
33
+ )
34
+ return sf
35
+
36
+ def test_debug_cleanup(sf_instance, already_executed: bool = False):
37
+ """
38
+ Test the debug_cleanup method of SFAuth.
39
+ This test will check if the method can be called without errors.
40
+ """
41
+ apex_logs = sf_instance.query("SELECT Id FROM ApexLog LIMIT 1")
42
+ apex_log_count = len(apex_logs.get("records", []))
43
+ if apex_log_count == 0:
44
+ # OK, so we need to create an Apex log to test cleanup
45
+ # To do this, let's execute a simple Apex anonymous block
46
+ import http.client
47
+ conn = http.client.HTTPSConnection(sf_instance.instance_url.replace("https://", ""))
48
+ conn.request(
49
+ "GET",
50
+ '/services/data/v64.0/tooling/executeAnonymous/?anonymousBody=Long%20currentUnixTime%20%3D%20DateTime.now().getTime()%20%2F%201000%3B',
51
+ headers={
52
+ "Authorization": f"Bearer {sf_instance.access_token}",
53
+ "Content-Type": "application/json"
54
+ }
55
+ )
56
+ response = conn.getresponse()
57
+ if response.status != http.client.OK:
58
+ pytest.fail(f"Failed to create Apex logs: {response.reason}")
59
+ if already_executed:
60
+ pytest.fail("ApexLog creation failed, cannot evaluate Apex log test.")
61
+ return test_debug_cleanup(sf_instance, already_executed=True)
62
+
63
+ sf_instance.debug_cleanup(apex_logs=True)
64
+
65
+ apex_logs = sf_instance.query("SELECT Id FROM ApexLog LIMIT 1")
66
+ apex_log_count = len(apex_logs.get("records", []))
67
+ assert apex_log_count == 0, "Apex logs were not cleaned up successfully."
@@ -3,5 +3,5 @@ requires-python = ">=3.9"
3
3
 
4
4
  [[package]]
5
5
  name = "sfq"
6
- version = "0.0.28"
6
+ version = "0.0.30"
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