scanii-python 1.0.0__tar.gz → 1.1.0__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.
@@ -17,9 +17,9 @@ jobs:
17
17
  python: ["3.13", "3.14"]
18
18
 
19
19
  steps:
20
- - uses: actions/checkout@v4
20
+ - uses: actions/checkout@v7
21
21
 
22
- - uses: actions/setup-python@v5
22
+ - uses: actions/setup-python@v7
23
23
  with:
24
24
  python-version: ${{ matrix.python }}
25
25
 
@@ -14,11 +14,11 @@ jobs:
14
14
  contents: read
15
15
 
16
16
  steps:
17
- - uses: actions/checkout@v4
17
+ - uses: actions/checkout@v7
18
18
  with:
19
19
  ref: ${{ github.event.release.tag_name }}
20
20
 
21
- - uses: actions/setup-python@v5
21
+ - uses: actions/setup-python@v7
22
22
  with:
23
23
  python-version: "3.13"
24
24
 
@@ -1,5 +1,33 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.1.0 — 2026-08-31
4
+
5
+ ### Added
6
+
7
+ - `ScaniiClient.delete(id)` — deletes a previously processed file result
8
+ (`DELETE /files/{id}`). Returns `True` on success; the processing trace is
9
+ left intact.
10
+ - `ScaniiClient.delete_trace(id)` — deletes the processing trace separately
11
+ (`DELETE /files/{id}/trace`). Returns `True` on success; the processing
12
+ result is left intact.
13
+
14
+ The two resources are independent: deleting one does not remove the other.
15
+ To erase a scan entirely, call both.
16
+
17
+ ### Changed
18
+
19
+ - Dropped the "v2.2 preview" designation from `retrieve_trace()` and
20
+ `process_from_url()` in the README and docstrings. Neither is marked preview
21
+ in the API contract; the methods themselves are unchanged.
22
+
23
+ ## 1.0.1 — 2026-08-15
24
+
25
+ ### Changed
26
+
27
+ - Bumped CI actions: `actions/checkout` v4 → v7, `actions/setup-python` v5 → v7.
28
+ (Dev dependencies are unpinned ranges resolved at install time; runtime
29
+ `dependencies` remains empty — the published package is unchanged.)
30
+
3
31
  ## 1.0.0 — 2026-05-05
4
32
 
5
33
  Initial release. Replaces the unmaintained `scanii.py` skeleton.
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: scanii-python
3
- Version: 1.0.0
3
+ Version: 1.1.0
4
4
  Summary: Zero-dependency Python SDK for the Scanii content security API
5
5
  Project-URL: Homepage, https://scanii.com
6
6
  Project-URL: Repository, https://github.com/scanii/scanii-python
@@ -85,14 +85,15 @@ Supply either `key` + `secret` (HTTP Basic Auth) or `token` (auth-token authenti
85
85
  | `process_async(content, filename, content_type=None, metadata=None, callback=None)` | Async-on-server scan of an IO-like object; returns `ScaniiPendingResult` |
86
86
  | `process_async_file(path, metadata=None, callback=None)` | Async-on-server scan of a file on disk; returns `ScaniiPendingResult` |
87
87
  | `retrieve(id)` | Retrieve a previous scan result |
88
+ | `delete(id)` | Delete a scan result. Returns `True`; the processing trace is left intact |
89
+ | `delete_trace(id)` | Delete a processing trace. Returns `True`; the scan result is left intact |
88
90
  | `fetch(url, metadata=None, callback=None)` | Server-side async fetch-and-scan of a remote URL |
91
+ | `process_from_url(location, callback=None, metadata=None)` | Synchronous scan of a remote URL via `POST /files` |
92
+ | `retrieve_trace(id)` | Retrieve processing event trace; returns `None` on 404 |
89
93
 
90
- ### v2.2 preview methods
91
-
92
- | Method | Description |
93
- |---|---|
94
- | `retrieve_trace(id)` **(v2.2 preview)** | Retrieve processing event trace; returns `None` on 404 |
95
- | `process_from_url(location, callback=None, metadata=None)` **(v2.2 preview)** | Synchronous scan of a remote URL via `POST /files` |
94
+ `delete()` and `delete_trace()` act on independent resources: deleting a scan result
95
+ leaves its trace readable, and deleting a trace leaves the result readable. To erase a
96
+ scan entirely, call both.
96
97
 
97
98
  ### Auth tokens
98
99
 
@@ -67,14 +67,15 @@ Supply either `key` + `secret` (HTTP Basic Auth) or `token` (auth-token authenti
67
67
  | `process_async(content, filename, content_type=None, metadata=None, callback=None)` | Async-on-server scan of an IO-like object; returns `ScaniiPendingResult` |
68
68
  | `process_async_file(path, metadata=None, callback=None)` | Async-on-server scan of a file on disk; returns `ScaniiPendingResult` |
69
69
  | `retrieve(id)` | Retrieve a previous scan result |
70
+ | `delete(id)` | Delete a scan result. Returns `True`; the processing trace is left intact |
71
+ | `delete_trace(id)` | Delete a processing trace. Returns `True`; the scan result is left intact |
70
72
  | `fetch(url, metadata=None, callback=None)` | Server-side async fetch-and-scan of a remote URL |
73
+ | `process_from_url(location, callback=None, metadata=None)` | Synchronous scan of a remote URL via `POST /files` |
74
+ | `retrieve_trace(id)` | Retrieve processing event trace; returns `None` on 404 |
71
75
 
72
- ### v2.2 preview methods
73
-
74
- | Method | Description |
75
- |---|---|
76
- | `retrieve_trace(id)` **(v2.2 preview)** | Retrieve processing event trace; returns `None` on 404 |
77
- | `process_from_url(location, callback=None, metadata=None)` **(v2.2 preview)** | Synchronous scan of a remote URL via `POST /files` |
76
+ `delete()` and `delete_trace()` act on independent resources: deleting a scan result
77
+ leaves its trace readable, and deleting a trace leaves the result readable. To erase a
78
+ scan entirely, call both.
78
79
 
79
80
  ### Auth tokens
80
81
 
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "scanii-python"
7
- version = "1.0.0"
7
+ version = "1.1.0"
8
8
  description = "Zero-dependency Python SDK for the Scanii content security API"
9
9
  readme = "README.md"
10
10
  license = "Apache-2.0"
@@ -0,0 +1 @@
1
+ __version__ = "1.1.0"
@@ -182,9 +182,6 @@ class ScaniiClient:
182
182
 
183
183
  Returns ``None`` when no trace exists for the given id (HTTP 404).
184
184
 
185
- This is a v2.2 preview surface; the API shape may shift before it is
186
- marked stable.
187
-
188
185
  :param id: processing id returned by :meth:`process` or :meth:`process_file`
189
186
  :see: https://scanii.github.io/openapi/v22/ GET /files/{id}/trace
190
187
  :return: :class:`~scanii.ScaniiTraceResult` or ``None``
@@ -214,9 +211,6 @@ class ScaniiClient:
214
211
  ``location`` must be a string URL — matches the existing :meth:`fetch`
215
212
  string-URL convention and the Java reference (``processFromUrl(String)``).
216
213
 
217
- This is a v2.2 preview surface; the API shape may shift before it is
218
- marked stable.
219
-
220
214
  :param location: URL of the content to scan
221
215
  :param callback: URL to POST the result to on completion
222
216
  :param metadata: arbitrary key/value pairs attached to the result
@@ -235,6 +229,45 @@ class ScaniiClient:
235
229
  self._raise_for_status(status, resp_body, headers, expected=201)
236
230
  return ScaniiProcessingResult.from_response(resp_body, headers)
237
231
 
232
+ def delete(self, id: str) -> bool:
233
+ """Delete a previously processed file result.
234
+
235
+ The processing trace is a separate resource and is **not** removed by
236
+ this call — it stays readable via :meth:`retrieve_trace` until you
237
+ delete it with :meth:`delete_trace`. To erase a scan entirely, call
238
+ both.
239
+
240
+ :param id: processing id returned by :meth:`process` or :meth:`process_file`
241
+ :see: https://scanii.github.io/openapi/v22/ DELETE /files/{id}
242
+ :return: ``True`` on success (HTTP 204)
243
+ :raises ScaniiError: when no result exists for the id (HTTP 404), which is
244
+ also what a repeated delete of the same id returns
245
+ """
246
+ if not id:
247
+ raise ValueError("id must not be empty")
248
+ status, resp_body, headers = self._request("DELETE", f"/files/{_urlencode(id)}")
249
+ self._raise_for_status(status, resp_body, headers, expected=204)
250
+ return True
251
+
252
+ def delete_trace(self, id: str) -> bool:
253
+ """Delete the processing trace for a previously processed file.
254
+
255
+ Leaves the processing result itself untouched.
256
+
257
+ :param id: processing id returned by :meth:`process` or :meth:`process_file`
258
+ :see: https://scanii.github.io/openapi/v22/ DELETE /files/{id}/trace
259
+ :return: ``True`` on success (HTTP 204)
260
+ :raises ScaniiError: when no trace exists for the id (HTTP 404), which is
261
+ also what a repeated delete of the same id returns
262
+ """
263
+ if not id:
264
+ raise ValueError("id must not be empty")
265
+ status, resp_body, headers = self._request(
266
+ "DELETE", f"/files/{_urlencode(id)}/trace"
267
+ )
268
+ self._raise_for_status(status, resp_body, headers, expected=204)
269
+ return True
270
+
238
271
  # ------------------------------------------------------------------
239
272
  # Other API methods
240
273
  # ------------------------------------------------------------------
@@ -29,9 +29,6 @@ class ScaniiTraceEvent:
29
29
  class ScaniiTraceResult:
30
30
  """Result of :meth:`~scanii.ScaniiClient.retrieve_trace`.
31
31
 
32
- This is a v2.2 preview surface; the API shape may shift before it is
33
- marked stable.
34
-
35
32
  See https://scanii.github.io/openapi/v22/
36
33
  """
37
34
 
@@ -203,6 +203,59 @@ class TestProcessFromUrl:
203
203
  assert "content.malicious.eicar-test-signature" in result.findings
204
204
 
205
205
 
206
+ # ---------------------------------------------------------------------------
207
+ # v2.2 surface — delete / delete_trace (hard-assert, no self-skip)
208
+ #
209
+ # The result and the trace are independent resources: deleting one must leave
210
+ # the other readable. These assertions are the whole point of the split, so they
211
+ # are deliberately strict.
212
+ # ---------------------------------------------------------------------------
213
+
214
+ class TestDelete:
215
+ UNKNOWN_ID = "00000000-0000-0000-0000-000000000000"
216
+
217
+ def test_delete_removes_result_and_leaves_trace(self, client):
218
+ result = client.process_file(make_clean_file())
219
+
220
+ assert client.delete(result.id) is True
221
+
222
+ with pytest.raises(ScaniiError):
223
+ client.retrieve(result.id)
224
+ assert client.retrieve_trace(result.id) is not None, (
225
+ "trace must survive deletion of the result"
226
+ )
227
+
228
+ def test_delete_trace_removes_trace_and_leaves_result(self, client):
229
+ result = client.process_file(make_clean_file())
230
+
231
+ assert client.delete_trace(result.id) is True
232
+
233
+ assert client.retrieve_trace(result.id) is None
234
+ assert client.retrieve(result.id).id == result.id, (
235
+ "result must survive deletion of the trace"
236
+ )
237
+
238
+ def test_repeated_delete_raises(self, client):
239
+ result = client.process_file(make_clean_file())
240
+ assert client.delete(result.id) is True
241
+ with pytest.raises(ScaniiError):
242
+ client.delete(result.id)
243
+
244
+ def test_delete_unknown_id_raises(self, client):
245
+ with pytest.raises(ScaniiError):
246
+ client.delete(self.UNKNOWN_ID)
247
+
248
+ def test_delete_trace_unknown_id_raises(self, client):
249
+ with pytest.raises(ScaniiError):
250
+ client.delete_trace(self.UNKNOWN_ID)
251
+
252
+ def test_delete_empty_id_raises_value_error(self, client):
253
+ with pytest.raises(ValueError):
254
+ client.delete("")
255
+ with pytest.raises(ValueError):
256
+ client.delete_trace("")
257
+
258
+
206
259
  # ---------------------------------------------------------------------------
207
260
  # Auth token lifecycle
208
261
  # ---------------------------------------------------------------------------
@@ -139,8 +139,10 @@ class TestScaniiTarget:
139
139
 
140
140
  class TestConstructor:
141
141
  def test_key_and_secret_accepted(self):
142
+ from scanii._version import __version__
143
+
142
144
  c = _make_client()
143
- assert c._user_agent == "scanii-python/1.0.0"
145
+ assert c._user_agent == f"scanii-python/{__version__}"
144
146
 
145
147
  def test_token_accepted(self):
146
148
  c = ScaniiClient(token="mytoken", target=TARGET)
@@ -445,6 +447,68 @@ class TestAuthTokens:
445
447
  _make_client().delete_auth_token("")
446
448
 
447
449
 
450
+ # ---------------------------------------------------------------------------
451
+ # delete / delete_trace
452
+ # ---------------------------------------------------------------------------
453
+
454
+ class TestDelete:
455
+ def test_delete_sends_delete_to_files_path(self):
456
+ mock_resp = _mock_response(204, "")
457
+ with patch("urllib.request.urlopen", return_value=mock_resp) as m:
458
+ assert _make_client().delete("abc") is True
459
+ req = m.call_args[0][0]
460
+ assert req.get_method() == "DELETE"
461
+ assert req.full_url == f"{ENDPOINT}/v2.2/files/abc"
462
+
463
+ def test_delete_trace_sends_delete_to_trace_path(self):
464
+ mock_resp = _mock_response(204, "")
465
+ with patch("urllib.request.urlopen", return_value=mock_resp) as m:
466
+ assert _make_client().delete_trace("abc") is True
467
+ req = m.call_args[0][0]
468
+ assert req.get_method() == "DELETE"
469
+ assert req.full_url == f"{ENDPOINT}/v2.2/files/abc/trace"
470
+
471
+ def test_delete_url_encodes_the_id(self):
472
+ mock_resp = _mock_response(204, "")
473
+ with patch("urllib.request.urlopen", return_value=mock_resp) as m:
474
+ _make_client().delete("a b/c")
475
+ req = m.call_args[0][0]
476
+ assert req.full_url == f"{ENDPOINT}/v2.2/files/a%20b%2Fc"
477
+
478
+ def test_delete_404_raises(self):
479
+ mock_resp = _mock_response(404, json.dumps({"error": "not found"}))
480
+ with patch("urllib.request.urlopen", return_value=mock_resp):
481
+ with pytest.raises(ScaniiError):
482
+ _make_client().delete("missing")
483
+
484
+ def test_delete_trace_404_raises(self):
485
+ mock_resp = _mock_response(404, json.dumps({"error": "no trace"}))
486
+ with patch("urllib.request.urlopen", return_value=mock_resp):
487
+ with pytest.raises(ScaniiError):
488
+ _make_client().delete_trace("missing")
489
+
490
+ def test_delete_403_raises_auth_error(self):
491
+ # Per the spec, a temporary auth token is not privileged to delete.
492
+ mock_resp = _mock_response(403, json.dumps({"error": "forbidden"}))
493
+ with patch("urllib.request.urlopen", return_value=mock_resp):
494
+ with pytest.raises(ScaniiAuthError):
495
+ _make_client().delete("abc")
496
+
497
+ def test_delete_trace_403_raises_auth_error(self):
498
+ mock_resp = _mock_response(403, json.dumps({"error": "forbidden"}))
499
+ with patch("urllib.request.urlopen", return_value=mock_resp):
500
+ with pytest.raises(ScaniiAuthError):
501
+ _make_client().delete_trace("abc")
502
+
503
+ def test_delete_empty_id_raises(self):
504
+ with pytest.raises(ValueError):
505
+ _make_client().delete("")
506
+
507
+ def test_delete_trace_empty_id_raises(self):
508
+ with pytest.raises(ValueError):
509
+ _make_client().delete_trace("")
510
+
511
+
448
512
  # ---------------------------------------------------------------------------
449
513
  # Deprecation warning on ScaniiProcessingResult.error
450
514
  # ---------------------------------------------------------------------------
@@ -1 +0,0 @@
1
- __version__ = "1.0.0"
File without changes
File without changes