Skip to content

Commit ad8a23f

Browse files
cigan1claude
andcommitted
Initial commit: siftfy 0.1.0
Sync (Siftfy) and async (AsyncSiftfy) clients for the Siftfy spam-classification API. Calibrated probability + likelihood bucket, typed exceptions (AuthenticationError/RateLimitError/APIError), exponential backoff with Retry-After honouring, pluggable httpx clients, and a custom base_url for self-hosted/staging targets. - Python 3.9+, MIT license, fully type-hinted (PEP 561 marker shipped). - httpx for transport, dataclass for the Prediction response. - 16 tests (sync + async) using respx, all passing. - Ruff + mypy strict, both clean. - GitHub Actions: matrix test on 3.9-3.13, then publish to PyPI on tag via trusted publishing. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
0 parents  commit ad8a23f

17 files changed

Lines changed: 1008 additions & 0 deletions

.github/workflows/ci.yml

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
branches: [main]
6+
pull_request:
7+
branches: [main]
8+
9+
jobs:
10+
test:
11+
runs-on: ubuntu-latest
12+
strategy:
13+
fail-fast: false
14+
matrix:
15+
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"]
16+
17+
steps:
18+
- uses: actions/checkout@v4
19+
20+
- name: Set up Python ${{ matrix.python-version }}
21+
uses: actions/setup-python@v5
22+
with:
23+
python-version: ${{ matrix.python-version }}
24+
cache: pip
25+
26+
- name: Install package + dev deps
27+
run: |
28+
python -m pip install --upgrade pip
29+
pip install -e ".[dev]"
30+
31+
- name: Lint
32+
run: ruff check src tests
33+
34+
- name: Type-check
35+
run: mypy src
36+
37+
- name: Test
38+
run: pytest -q

.github/workflows/publish.yml

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
name: Publish to PyPI
2+
3+
# Trigger on a tag like v0.1.0. The workflow verifies that the tag matches
4+
# the version declared in src/siftfy/_version.py before publishing, so we can
5+
# never ship a tag that's out of sync with the package metadata.
6+
7+
on:
8+
push:
9+
tags:
10+
- "v*"
11+
12+
jobs:
13+
publish:
14+
runs-on: ubuntu-latest
15+
environment: pypi
16+
permissions:
17+
id-token: write # PyPI trusted publishing (no API token needed)
18+
contents: read
19+
20+
steps:
21+
- uses: actions/checkout@v4
22+
23+
- name: Set up Python
24+
uses: actions/setup-python@v5
25+
with:
26+
python-version: "3.12"
27+
28+
- name: Verify tag matches package version
29+
run: |
30+
TAG="${GITHUB_REF_NAME#v}"
31+
PKG_VERSION=$(python -c "import re,pathlib;print(re.search(r'\"([^\"]+)\"', pathlib.Path('src/siftfy/_version.py').read_text()).group(1))")
32+
if [ "$TAG" != "$PKG_VERSION" ]; then
33+
echo "tag $GITHUB_REF_NAME ($TAG) does not match _version.py ($PKG_VERSION)" >&2
34+
exit 1
35+
fi
36+
37+
- name: Install build tooling
38+
run: |
39+
python -m pip install --upgrade pip
40+
pip install build
41+
42+
- name: Build sdist + wheel
43+
run: python -m build
44+
45+
- name: Publish to PyPI
46+
uses: pypa/gh-action-pypi-publish@release/v1

.gitignore

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
# Python
2+
__pycache__/
3+
*.py[cod]
4+
*$py.class
5+
*.so
6+
.Python
7+
build/
8+
dist/
9+
*.egg-info/
10+
*.egg
11+
12+
# Tooling
13+
.pytest_cache/
14+
.mypy_cache/
15+
.ruff_cache/
16+
.coverage
17+
htmlcov/
18+
19+
# Virtualenvs
20+
.venv/
21+
venv/
22+
23+
# Editors / OS
24+
.vscode/
25+
.idea/
26+
.DS_Store
27+
28+
# Env
29+
.env
30+
.env.local

LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2026 Siftfy
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

README.md

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
# siftfy-python
2+
3+
Official Python client for the [Siftfy](https://siftfy.io) spam-classification
4+
API. POST text, get a calibrated spam probability back. One round-trip, no
5+
queues, no models to host.
6+
7+
[![PyPI version](https://img.shields.io/pypi/v/siftfy.svg)](https://pypi.org/project/siftfy/)
8+
[![Python versions](https://img.shields.io/pypi/pyversions/siftfy.svg)](https://pypi.org/project/siftfy/)
9+
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
10+
11+
## Install
12+
13+
```bash
14+
pip install siftfy
15+
```
16+
17+
## Quick start
18+
19+
```python
20+
from siftfy import Siftfy
21+
22+
client = Siftfy(api_key="sk_live_...")
23+
24+
result = client.predict("Win a free iPad — click here!")
25+
print(result.spam_probability) # 0.97
26+
print(result.likelihood) # "high"
27+
```
28+
29+
Get an API key at [siftfy.io](https://siftfy.io) — the free tier covers
30+
10,000 requests/month at no cost.
31+
32+
## Async
33+
34+
```python
35+
import asyncio
36+
from siftfy import AsyncSiftfy
37+
38+
async def main() -> None:
39+
async with AsyncSiftfy(api_key="sk_live_...") as client:
40+
result = await client.predict("hello, world")
41+
print(result.spam_probability)
42+
43+
asyncio.run(main())
44+
```
45+
46+
## Calibrated probabilities
47+
48+
Every `spam_probability` is a calibrated value between 0 and 1 — at 0.7,
49+
roughly 70% of inputs with that score are actually spam. Pick a threshold
50+
appropriate to your use case (a help-desk form tolerates more false positives
51+
than a marketplace listing); the same model serves both.
52+
53+
The `likelihood` field is a coarse bucket (`"low"`, `"medium"`, `"high"`)
54+
derived from the probability. Handy for quick branches, but for production
55+
decisions thread on the raw probability and your own threshold.
56+
57+
## Errors
58+
59+
```python
60+
from siftfy import (
61+
Siftfy,
62+
AuthenticationError, # 401 — bad / revoked key
63+
RateLimitError, # 429 — over your tier limit; .retry_after available
64+
APIError, # any other 4xx/5xx
65+
SiftfyError, # network / request transport errors
66+
)
67+
68+
try:
69+
result = client.predict(text)
70+
except RateLimitError as e:
71+
sleep_for = e.retry_after or 1.0
72+
...
73+
except AuthenticationError:
74+
...
75+
except APIError as e:
76+
log(f"siftfy error {e.status_code}: {e} (request_id={e.request_id})")
77+
```
78+
79+
The client retries idempotent failures (HTTP 408 / 429 / 5xx, network errors)
80+
with exponential backoff and jitter, honouring `Retry-After` when present.
81+
Tune with `max_retries=N` (default 2; set 0 to disable).
82+
83+
## Configuration
84+
85+
```python
86+
client = Siftfy(
87+
api_key="sk_live_...",
88+
base_url="https://api.siftfy.io", # override for self-hosted / staging
89+
timeout=10.0, # seconds, applied per attempt
90+
max_retries=2, # 0 disables retries
91+
)
92+
```
93+
94+
You can also pass your own `httpx.Client` (or `httpx.AsyncClient` for the
95+
async client) via `http_client=...` if you want connection pooling, custom
96+
transports, or to share a client across services.
97+
98+
## Resources
99+
100+
- API reference: <https://siftfy.io/docs>
101+
- Pricing: <https://siftfy.io/pricing>
102+
- Status: ping `https://api.siftfy.io/health`
103+
- Issues: <https://github.com/GipsyChef/siftfy-python/issues>
104+
105+
## License
106+
107+
MIT — see [LICENSE](LICENSE).

pyproject.toml

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
[build-system]
2+
requires = ["hatchling>=1.24"]
3+
build-backend = "hatchling.build"
4+
5+
[project]
6+
name = "siftfy"
7+
version = "0.1.0"
8+
description = "Official Python client for the Siftfy spam-classification API."
9+
readme = "README.md"
10+
license = { text = "MIT" }
11+
requires-python = ">=3.9"
12+
authors = [{ name = "Siftfy", email = "hi@siftfy.io" }]
13+
keywords = [
14+
"spam",
15+
"spam-detection",
16+
"spam-classifier",
17+
"content-moderation",
18+
"api-client",
19+
"siftfy",
20+
]
21+
classifiers = [
22+
"Development Status :: 4 - Beta",
23+
"Intended Audience :: Developers",
24+
"License :: OSI Approved :: MIT License",
25+
"Operating System :: OS Independent",
26+
"Programming Language :: Python :: 3",
27+
"Programming Language :: Python :: 3 :: Only",
28+
"Programming Language :: Python :: 3.9",
29+
"Programming Language :: Python :: 3.10",
30+
"Programming Language :: Python :: 3.11",
31+
"Programming Language :: Python :: 3.12",
32+
"Programming Language :: Python :: 3.13",
33+
"Topic :: Communications :: Email :: Filters",
34+
"Topic :: Internet :: WWW/HTTP",
35+
"Topic :: Software Development :: Libraries :: Python Modules",
36+
"Typing :: Typed",
37+
]
38+
dependencies = [
39+
"httpx>=0.25",
40+
]
41+
42+
[project.urls]
43+
Homepage = "https://siftfy.io"
44+
Documentation = "https://siftfy.io/docs"
45+
Source = "https://github.com/GipsyChef/siftfy-python"
46+
Issues = "https://github.com/GipsyChef/siftfy-python/issues"
47+
Changelog = "https://github.com/GipsyChef/siftfy-python/releases"
48+
49+
[project.optional-dependencies]
50+
dev = [
51+
"pytest>=8",
52+
"pytest-asyncio>=0.23",
53+
"respx>=0.21",
54+
"ruff>=0.6",
55+
"mypy>=1.10",
56+
]
57+
58+
[tool.hatch.build.targets.wheel]
59+
packages = ["src/siftfy"]
60+
61+
[tool.hatch.build.targets.sdist]
62+
include = ["src/siftfy", "README.md", "LICENSE", "pyproject.toml"]
63+
64+
[tool.pytest.ini_options]
65+
testpaths = ["tests"]
66+
asyncio_mode = "auto"
67+
68+
[tool.ruff]
69+
line-length = 100
70+
target-version = "py39"
71+
72+
[tool.ruff.lint]
73+
select = ["E", "F", "I", "B", "UP", "RUF"]
74+
75+
[tool.mypy]
76+
python_version = "3.9"
77+
strict = true
78+
files = ["src/siftfy"]

src/siftfy/__init__.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
"""Official Python client for the Siftfy spam-classification API.
2+
3+
>>> from siftfy import Siftfy
4+
>>> client = Siftfy(api_key="sk_live_...")
5+
>>> result = client.predict("Win a free iPad — click here!")
6+
>>> result.spam_probability
7+
0.97
8+
>>> result.likelihood
9+
'high'
10+
"""
11+
12+
from siftfy._version import __version__
13+
from siftfy.async_client import AsyncSiftfy
14+
from siftfy.client import Siftfy
15+
from siftfy.exceptions import (
16+
APIError,
17+
AuthenticationError,
18+
RateLimitError,
19+
SiftfyError,
20+
)
21+
from siftfy.models import Likelihood, Prediction
22+
23+
__all__ = [
24+
"APIError",
25+
"AsyncSiftfy",
26+
"AuthenticationError",
27+
"Likelihood",
28+
"Prediction",
29+
"RateLimitError",
30+
"Siftfy",
31+
"SiftfyError",
32+
"__version__",
33+
]

0 commit comments

Comments
 (0)