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
22 changes: 22 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
root = true

[*]
charset = utf-8
end_of_line = lf
indent_style = space
indent_size = 4
insert_final_newline = true
trim_trailing_whitespace = true

[*.{yml,yaml,toml,json}]
indent_size = 2

[*.md]
indent_size = 2
trim_trailing_whitespace = false

# Shared test fixtures are compared byte for byte.
[tests/data/**]
insert_final_newline = unset
trim_trailing_whitespace = unset
indent_size = unset
1 change: 1 addition & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
tests/data/** -text
62 changes: 57 additions & 5 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,68 @@ on:
branches: [main]
pull_request:

permissions:
contents: read

concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true

jobs:
test:
name: Python ${{ matrix.python-version }}
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"]
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
with:
persist-credentials: false
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
with:
python-version: ${{ matrix.python-version }}
- run: pip install -e ".[dev]"
- run: pytest -q
- name: Install
run: |
python -m pip install --upgrade pip
python -m pip install -e ".[dev]"
- name: Lint
run: |
ruff check .
ruff format --check .
- name: Type check
run: mypy --strict src
- name: Test
run: pytest -q --cov=shieldlabs --cov-report=term-missing

example:
name: FastAPI example
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
with:
persist-credentials: false
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
with:
python-version: "3.12"
- name: Install
run: python -m pip install -e ".[dev]" -r examples/requirements.txt
- name: Smoke test
run: pytest -q tests/test_example_app.py

build:
name: Build distribution
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
with:
persist-credentials: false
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
with:
python-version: "3.12"
- name: Build
run: |
python -m pip install build==1.6.1 twine==7.0.0
python -m build
twine check --strict dist/*
25 changes: 0 additions & 25 deletions .github/workflows/publish.yml

This file was deleted.

70 changes: 70 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
name: Release

# Publishes the package to PyPI when a version tag (v1.0.0, v1.0.1, ...) is pushed.
# The build job has read-only access. The publish job only receives the built files and uses
# PyPI trusted publishing (OpenID Connect), so no API token is stored in the repository.
# One-time setup: on PyPI, add this repository, the workflow file release.yml and the
# environment "pypi" as a trusted publisher of the "shieldlabs" project.
# Re-running the workflow for the same tag is safe: files already on PyPI are skipped.

on:
push:
tags: ["v*"]

permissions:
contents: read

jobs:
build:
name: Check and build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
with:
persist-credentials: false
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
with:
python-version: "3.12"
- name: Install
run: python -m pip install -e ".[dev]" build==1.6.1 twine==7.0.0
- name: Check that the tag matches the package version
run: |
version="$(python -c 'import shieldlabs; print(shieldlabs.__version__)')"
if [ "v${version}" != "${GITHUB_REF_NAME}" ]; then
echo "::error::Tag ${GITHUB_REF_NAME} does not match the package version ${version}."
exit 1
fi
- name: Verify
run: |
ruff check .
ruff format --check .
mypy --strict src
pytest -q
- name: Build
run: |
python -m build
twine check --strict dist/*
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: dist
path: dist/
if-no-files-found: error

publish:
name: Publish to PyPI
needs: build
runs-on: ubuntu-latest
environment:
name: pypi
url: https://pypi.org/project/shieldlabs/
permissions:
id-token: write # trusted publishing and attestations
steps:
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
with:
name: dist
path: dist/
- uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2
with:
skip-existing: true
attestations: true
28 changes: 21 additions & 7 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,16 +1,30 @@
# Python
__pycache__/
*.py[cod]
dist/
build/
*.egg-info/
.eggs/
build/
dist/

# Virtual environments
.venv/
venv/
.env
.env.*
.DS_Store

# Tooling caches and reports
.pytest_cache/
.mypy_cache/
.ruff_cache/
.coverage
.coverage.*
coverage.xml
htmlcov/

# Local configuration
.env
.env.*

src/shieldlabs.egg-info
.history
# Editors and OS
.DS_Store
.history/
.idea/
.vscode/
64 changes: 62 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,65 @@
# Changelog

## 2026-09-06
All notable changes to this project are documented in this file. The format follows
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and the project uses
[Semantic Versioning](https://semver.org/spec/v2.0.0.html).

- Minor improvements and bug fixes
## [Unreleased]

## [1.0.0] - 2026-09-30

First stable release. It replaces the 0.1.0 preview package completely.

### Added

- `ShieldLabs` and `AsyncShieldLabs` History API clients: `history.search`, `history.iter`
(de-duplicated on `request_id`) and `identifications.get`, which waits for the verdict:
- `timeout` (10 s by default) is the total budget of the call. Polls run immediately, then
after waits of `poll_interval` times 1, 2, 4, 6 and 8, then 8 again, each capped at 2 s, or
at `poll_interval` when that is longer (0.25 s, 0.5 s, 1 s, 1.5 s and every 2 s by default;
every 3 s for `poll_interval=3`), and a last time at the deadline.
- Each poll is one HTTP attempt with a timeout of the client timeout cut to the time left,
but at least 1 s.
- A `429`, a 5xx response, a connection error or a timeout keeps it polling. The error of the
last poll is raised at the deadline; `None` means the last poll found no row.
- After a `429` the next wait is the longest of the ladder step, 1 s and `Retry-After`
capped at 10 s (`Retry-After: 0` or a past date counts as 0), cut to the deadline. A capped
`Retry-After` longer than the time left is raised at once.
- `400`, `401`, `403` and `404` end the wait at once.
- `ShieldLabsManagement` and `AsyncShieldLabsManagement` with `get_profile()`. The domain is
normalized before it is sent, and a `429` is never retried.
- `webhooks.verify_signature` and `webhooks.construct_event`. Both accept one signing secret or
a list of secrets for rotation. Events are typed: `IdentificationScoredEvent`,
`WebhookPingEvent` and `UnknownWebhookEvent`.
- One `Identification` model for webhook data and History rows: 19 detection flags, risk
signals (with descriptions on History rows), timezone-aware `observed_at` and the original
payload in `raw`. Also `DomainProfile`, `HistoryPage` and `SignalName`.
- Helpers: `risk_band`, `is_rate_limited`, `evaluate_identification` and `user_hid`.
- Error hierarchy including `QuotaExceededError`; retries with jittered exponential backoff and
`Retry-After` as sent, up to 10 s (at least 1 s after a `429` without it); a
`User-Agent: shieldlabs-python/<version>` header.
- Client-side validation of every History lookup. User HIDs are percent-encoded in the form the
History API matches, and values that cannot be matched in the request path (`.`, `..` and
anything that contains `/`) raise `ValidationError` instead of returning an empty page.
- Base URLs must use https; plain http is accepted only for `localhost`, `127.0.0.1` and `::1`.
- A FastAPI example, the shared test fixtures and CI on Python 3.9 to 3.13.

### Changed

- History requests go to `https://account.shieldlabs.ai/api/v1/history/...`. A custom base URL
that ends in `/api` is accepted and the suffix is removed, so requests never reach
`/api/api/...` (the preview built that URL and received a 404).
- httpx is the only runtime dependency. Python 3.9 is the minimum version.

### Removed

- `verify_webhook` and `ShieldLabsClient` from the preview. Use `webhooks.verify_signature` and
`ShieldLabs().history.search` instead.

## [0.1.0] - 2026-09-06

Preview package with `verify_webhook` and a minimal History API client.

[Unreleased]: https://github.com/ShieldLabs-ai/shieldlabs-python/compare/v1.0.0...HEAD
[1.0.0]: https://github.com/ShieldLabs-ai/shieldlabs-python/releases/tag/v1.0.0
[0.1.0]: https://github.com/ShieldLabs-ai/shieldlabs-python/commits/main
64 changes: 64 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# Contributing

Thank you for helping improve the ShieldLabs Python SDK.

## Set up

Check out the repository, then:

```bash
python3 -m venv .venv
source .venv/bin/activate
pip install -e ".[dev]"
```

## Before you open a pull request

Run the same checks as CI:

```bash
ruff check . && ruff format --check . && mypy --strict src
pytest -q --cov=shieldlabs --cov-report=term-missing
```

- Every change comes with tests. Line coverage stays at 90% or above (CI enforces it).
- Code must run on Python 3.9: use `typing.Optional` and `typing.Union` in annotations.
- Keep httpx the only runtime dependency.
- Changes to the public API need an entry under `Unreleased` in `CHANGELOG.md`.
- The development tools have upper version bounds in `pyproject.toml`, so a new tool release
cannot change CI results on its own. Raise a bound in its own pull request.
- The FastAPI example has its own smoke test:
`pip install -r examples/requirements.txt && pytest tests/test_example_app.py`.

## Shared test fixtures

`tests/data/` holds the test fixtures that every ShieldLabs server SDK passes: History API
bodies, webhook bodies, signature vectors, error responses and the expected normalized results.
They are identical in every SDK and compared byte for byte (the `.raw.txt` files are exact
webhook bodies without a trailing newline), so do not edit them in a pull request. If a fixture
looks wrong, open an issue.

## Writing style

Docs, docstrings and comments use plain technical English and the terms used in the README.

## Commits

Use conventional commit messages: `feat:`, `fix:`, `docs:`, `test:`, `ci:`, `chore:`.

## Releasing

Maintainers bump `src/shieldlabs/_version.py`, move the `Unreleased` notes under a new version
heading in `CHANGELOG.md`, and push a `vX.Y.Z` tag. The release workflow checks that the tag
matches the version, runs the checks, builds the package once and publishes that build to PyPI
with trusted publishing and attestations, from a separate job that runs in the `pypi`
environment.

One-time setup: on PyPI, add this repository, the workflow file `release.yml` and the
environment `pypi` as a trusted publisher of the `shieldlabs` project; on GitHub, protect the
`pypi` environment with required reviewers. No API token is stored in the repository.
Re-running the workflow for a tag is safe: files that are already on PyPI are skipped.

## Security

Please report vulnerabilities privately to contact@shieldlabs.ai instead of opening an issue.
2 changes: 1 addition & 1 deletion LICENSE
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
MIT License

Copyright (c) 2026 ShieldLabs
Copyright (c) 2026 ShieldLabs Inc.

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
Expand Down
21 changes: 0 additions & 21 deletions PUBLISHING.md

This file was deleted.

Loading
Loading