Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions .github/workflows/pr.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
name: PR

on: pull_request

concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true

jobs:
verify:
name: Python ${{ matrix.python }} on ${{ matrix.os }}
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
python: ["3.13", "3.14"]

steps:
- uses: actions/checkout@v4

- uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python }}

- name: Setup scanii-cli
uses: scanii/setup-cli-action@v1

- name: Install dependencies
run: pip install -e ".[dev]"

- name: Run tests
run: pytest
env:
SCANII_TEST_ENDPOINT: http://localhost:4000

- name: Type check
run: mypy src

- name: Lint
run: ruff check

- name: Build
run: |
pip install build
python -m build
31 changes: 31 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
name: Release

on:
release:
types: [published]

jobs:
publish:
name: Publish to PyPI
runs-on: ubuntu-latest
environment: pypi
permissions:
id-token: write
contents: read

steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.release.tag_name }}

- uses: actions/setup-python@v5
with:
python-version: "3.13"

- name: Build
run: |
python -m pip install --upgrade pip build
python -m build

- name: Publish to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
15 changes: 13 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,15 @@
*.pyc
__pycache__/
*.py[cod]
*.egg-info/
dist/
build/
.venv/
venv/
.mypy_cache/
.ruff_cache/
.pytest_cache/
.coverage
htmlcov/
.DS_Store
*.db
*.swp
.DS_Store
48 changes: 48 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# Changelog

## 1.0.0 — 2026-05-05

Initial release. Replaces the unmaintained `scanii.py` skeleton.

**Reference:** scanii-java v8.1.0 (includes v2.2 API surface).

### API surface

- `ScaniiClient(*, key, secret, target, timeout)` — synchronous client, zero runtime dependencies, stdlib only; `target` is a required `ScaniiTarget` instance
- `process(content, filename, content_type, metadata, callback)` — stream-first synchronous scan; `content` is any object with `read(n) -> bytes`
- `process_file(path, metadata, callback)` — path convenience; opens the file and delegates to `process`
- `process_async(content, filename, content_type, metadata, callback)` — stream-first async-on-server submission
- `process_async_file(path, metadata, callback)` — path convenience for async submission
- `retrieve_trace(id)` — **(v2.2 preview)** retrieve processing event trace; returns `None` on 404
- `process_from_url(location, callback, metadata)` — **(v2.2 preview)** synchronous URL submission via `POST /files`
- `fetch(url, metadata, callback)` — async server-side fetch-and-scan via `POST /files/fetch`
- `retrieve(id)` — retrieve a previous scan result
- `ping()` — health check
- `create_auth_token(timeout_seconds)` / `retrieve_auth_token(id)` / `delete_auth_token(id)` — auth token lifecycle

### Streaming design

True socket-level streaming for `process` and `process_file` — uploads of any size do not buffer the full body in memory. Matches the cross-SDK streaming standard.

The multipart encoder builds a chained reader (prologue bytes → caller's IO → epilogue bytes) without reading the file body at construction time. The file content is transferred directly to the socket as urllib reads from the chain. `process_file(path)` streams from disk through the chained reader to the socket without intermediate buffering. IOs without `fileno()` or `seek`/`tell` (pipes, generators) buffer through `SpooledTemporaryFile(max_size=1 MiB)` — small payloads stay in memory, larger ones spill to disk.

`process(content, filename)` accepts any IO-like object (`io.BytesIO`, open file handles, network streams). `process_file(path)` is a thin wrapper for the common disk-file case. There is exactly one HTTP-handling code path; the file convenience is syntactic sugar.

### Error handling

- `ScaniiError` — base exception; carries `status_code`, `request_id`, `host_id`, `body`
- `ScaniiAuthError(ScaniiError)` — HTTP 401/403
- `ScaniiRateLimitError(ScaniiError)` — HTTP 429; carries `retry_after` (seconds) when the server provides it
- Network-level failures (`urllib.error.URLError`) propagate unwrapped per SDK Principle 3

### Deprecations (from day one)

- `ScaniiProcessingResult.error` — ships deprecated from day one. The server never populates this field on successful responses; errors arrive as non-2xx HTTP responses that raise `ScaniiError` subclasses. Constructing the dataclass with a non-`None` `error` value emits a `DeprecationWarning`. Will be removed in a future major version.

### Regional endpoints

Explicit `ScaniiTarget` required at construction. Regional endpoints are typed constants on `ScaniiTarget` (`US1`, `EU1`, `EU2`, `AP1`, `AP2`, `CA1`). The constructor takes a `target: ScaniiTarget` parameter with no default — customers must choose explicitly. For testing against scanii-cli or other local mocks, construct `ScaniiTarget("http://localhost:4000")` directly. Latency-based routing (`AUTO` in other Scanii SDKs) is intentionally not provided in Python; chain-of-custody and data-residency compliance require an explicit regional choice.

### Notes

- Published as `scanii-python` on PyPI; import name is `scanii`
Loading
Loading