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.
- {scanii_python-1.0.0 → scanii_python-1.1.0}/.github/workflows/pr.yml +2 -2
- {scanii_python-1.0.0 → scanii_python-1.1.0}/.github/workflows/release.yml +2 -2
- {scanii_python-1.0.0 → scanii_python-1.1.0}/CHANGELOG.md +28 -0
- {scanii_python-1.0.0 → scanii_python-1.1.0}/PKG-INFO +9 -8
- {scanii_python-1.0.0 → scanii_python-1.1.0}/README.md +7 -6
- {scanii_python-1.0.0 → scanii_python-1.1.0}/pyproject.toml +1 -1
- scanii_python-1.1.0/src/scanii/_version.py +1 -0
- {scanii_python-1.0.0 → scanii_python-1.1.0}/src/scanii/client.py +39 -6
- {scanii_python-1.0.0 → scanii_python-1.1.0}/src/scanii/models.py +0 -3
- {scanii_python-1.0.0 → scanii_python-1.1.0}/tests/test_client_integration.py +53 -0
- {scanii_python-1.0.0 → scanii_python-1.1.0}/tests/test_client_unit.py +65 -1
- scanii_python-1.0.0/src/scanii/_version.py +0 -1
- {scanii_python-1.0.0 → scanii_python-1.1.0}/.gitignore +0 -0
- {scanii_python-1.0.0 → scanii_python-1.1.0}/LICENSE +0 -0
- {scanii_python-1.0.0 → scanii_python-1.1.0}/src/scanii/__init__.py +0 -0
- {scanii_python-1.0.0 → scanii_python-1.1.0}/src/scanii/_multipart.py +0 -0
- {scanii_python-1.0.0 → scanii_python-1.1.0}/src/scanii/errors.py +0 -0
- {scanii_python-1.0.0 → scanii_python-1.1.0}/src/scanii/py.typed +0 -0
- {scanii_python-1.0.0 → scanii_python-1.1.0}/src/scanii/target.py +0 -0
- {scanii_python-1.0.0 → scanii_python-1.1.0}/tests/__init__.py +0 -0
|
@@ -14,11 +14,11 @@ jobs:
|
|
|
14
14
|
contents: read
|
|
15
15
|
|
|
16
16
|
steps:
|
|
17
|
-
- uses: actions/checkout@
|
|
17
|
+
- uses: actions/checkout@v7
|
|
18
18
|
with:
|
|
19
19
|
ref: ${{ github.event.release.tag_name }}
|
|
20
20
|
|
|
21
|
-
- uses: actions/setup-python@
|
|
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.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: scanii-python
|
|
3
|
-
Version: 1.
|
|
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
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
|
|
@@ -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/
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|