Skip to content

Commit 5271554

Browse files
leliaclaude
andcommitted
Add cached/query param support and 202 handling to diffscans.get
DiffScans.get now accepts optional query params (cached, omit_unchanged, omit_license_details) and returns a {"status": "processing", "id": ...} dict on HTTP 202 instead of logging an error, so clients can poll GET /orgs/{org}/diff-scans/{id}?cached=true until the computed diff is ready rather than holding a single idle connection open while the backend computes (which idle-timeout middleboxes like Azure NAT gateways kill after ~4 minutes). Also encode list-valued query params (e.g. committers) as repeated params in create_from_repo/create_from_ids via urlencode(doseq=True). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 1c1f1f2 commit 5271554

5 files changed

Lines changed: 99 additions & 7 deletions

File tree

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
44

55
[project]
66
name = "socketdev"
7-
version = "3.3.0"
7+
version = "3.4.0"
88
requires-python = ">= 3.9"
99
dependencies = [
1010
'requests',

socketdev/diffscans/__init__.py

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,41 @@ def list(self, org_slug: str, params: Optional[Dict[str, Any]] = None) -> dict:
2121
log.error(f"Error listing diff scans: {response.status_code}, message: {response.text}")
2222
return {}
2323

24-
def get(self, org_slug: str, diff_scan_id: str) -> dict:
25-
"""Fetch a diff scan by ID."""
24+
def get(self, org_slug: str, diff_scan_id: str, params: Optional[Dict[str, Any]] = None) -> dict:
25+
"""Fetch a diff scan by ID.
26+
27+
Args:
28+
org_slug: Organization slug
29+
diff_scan_id: The ID of the diff scan to fetch
30+
params: Optional query parameters. Supports:
31+
cached: When "true", return pre-computed results immediately
32+
(200) or a processing status (202) instead of holding
33+
the connection open while the diff is computed.
34+
omit_unchanged: When "true", omit unchanged artifacts.
35+
omit_license_details: When "true", omit license details.
36+
37+
Returns:
38+
dict: On 200, the API response containing the diff_scan object.
39+
On 202 (results still processing when cached=true), a dict of
40+
{"status": "processing", "id": diff_scan_id} so callers can
41+
poll until the diff scan is ready. Empty dict on error.
42+
"""
43+
import urllib.parse
2644
path = f"orgs/{org_slug}/diff-scans/{diff_scan_id}"
45+
if params:
46+
path += "?" + urllib.parse.urlencode(params, doseq=True)
2747
response = self.api.do_request(path=path, method="GET")
2848
if response.status_code == 200:
2949
return response.json()
50+
if response.status_code == 202:
51+
result = {"status": "processing", "id": diff_scan_id}
52+
try:
53+
body = response.json()
54+
if isinstance(body, dict):
55+
result.update(body)
56+
except ValueError:
57+
pass
58+
return result
3059
log.error(f"Error fetching diff scan: {response.status_code}, message: {response.text}")
3160
return {}
3261

@@ -61,7 +90,8 @@ def create_from_repo(self, org_slug: str, repo_slug: str, files: list, params: O
6190
import urllib.parse
6291
path = f"orgs/{org_slug}/diff-scans/from-repo/{repo_slug}"
6392
if params:
64-
path += "?" + urllib.parse.urlencode(params)
93+
# doseq=True so list values (e.g. committers) become repeated params
94+
path += "?" + urllib.parse.urlencode(params, doseq=True)
6595

6696
# Use lazy loading if requested
6797
if use_lazy_loading:
@@ -80,7 +110,7 @@ def create_from_ids(self, org_slug: str, params: Dict[str, Any]) -> dict:
80110
import urllib.parse
81111
path = f"orgs/{org_slug}/diff-scans/from-ids"
82112
if params:
83-
path += "?" + urllib.parse.urlencode(params)
113+
path += "?" + urllib.parse.urlencode(params, doseq=True)
84114
response = self.api.do_request(path=path, method="POST")
85115
if response.status_code in (200, 201):
86116
return response.json()

socketdev/version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
__version__ = "3.3.0"
1+
__version__ = "3.4.0"

tests/unit/test_all_endpoints_unit.py

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,45 @@ def test_diffscans_get_unit(self):
113113
self.assertEqual(call_args[0][0], "GET")
114114
self.assertIn("/orgs/test-org/diff-scans/diff-123", call_args[0][1])
115115

116+
def test_diffscans_get_cached_unit(self):
117+
"""Test diffscans get passes cached/omit params through as a query string."""
118+
expected_data = {
119+
"diff_scan": {
120+
"id": "diff-123",
121+
"artifacts": {"added": [], "removed": [], "unchanged": [], "replaced": [], "updated": []},
122+
}
123+
}
124+
self._mock_response(expected_data)
125+
126+
result = self.sdk.diffscans.get("test-org", "diff-123", params={"cached": "true"})
127+
128+
self.assertEqual(result, expected_data)
129+
call_args = self.mock_requests.request.call_args
130+
self.assertEqual(call_args[0][0], "GET")
131+
self.assertIn("/orgs/test-org/diff-scans/diff-123?cached=true", call_args[0][1])
132+
133+
def test_diffscans_get_processing_unit(self):
134+
"""Test diffscans get surfaces 202 processing status instead of an error."""
135+
self._mock_response({"status": "processing", "id": "diff-123"}, 202)
136+
137+
result = self.sdk.diffscans.get("test-org", "diff-123", params={"cached": "true"})
138+
139+
self.assertEqual(result.get("status"), "processing")
140+
self.assertEqual(result.get("id"), "diff-123")
141+
142+
def test_diffscans_get_processing_empty_body_unit(self):
143+
"""Test diffscans get synthesizes the processing status when the 202 body is empty."""
144+
mock_response = Mock()
145+
mock_response.status_code = 202
146+
mock_response.headers = {}
147+
mock_response.json.side_effect = ValueError("no body")
148+
mock_response.text = ""
149+
self.mock_requests.request.return_value = mock_response
150+
151+
result = self.sdk.diffscans.get("test-org", "diff-123", params={"cached": "true"})
152+
153+
self.assertEqual(result, {"status": "processing", "id": "diff-123"})
154+
116155
def test_diffscans_create_from_ids_unit(self):
117156
"""Test diffscans creation from scan IDs."""
118157
expected_data = {"id": "new-diff-scan", "status": "queued"}
@@ -153,6 +192,29 @@ def test_diffscans_create_from_repo_unit(self):
153192
finally:
154193
os.unlink(f.name)
155194

195+
def test_diffscans_create_from_repo_committers_list_unit(self):
196+
"""Test list-valued params (committers) encode as repeated query params."""
197+
self._mock_response({"id": "repo-diff-scan"}, 201)
198+
199+
with tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False) as f:
200+
json.dump({"name": "test", "version": "1.0.0"}, f)
201+
f.flush()
202+
203+
try:
204+
with open(f.name, "rb") as file_obj:
205+
files = [("file", ("package.json", file_obj))]
206+
params = {"committers": ["alice", "bob"], "branch": "main"}
207+
self.sdk.diffscans.create_from_repo("test-org", "test-repo", files, params)
208+
209+
call_args = self.mock_requests.request.call_args
210+
url = call_args[0][1]
211+
self.assertIn("committers=alice", url)
212+
self.assertIn("committers=bob", url)
213+
self.assertIn("branch=main", url)
214+
215+
finally:
216+
os.unlink(f.name)
217+
156218
def test_diffscans_gfm_unit(self):
157219
"""Test diffscans GitHub Flavored Markdown export."""
158220
expected_data = {"markdown": "# Diff Report\n\n## Summary\n- Added: 0\n- Removed: 0"}

uv.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)