Oneport ApiDiff
Breaking-change gate for PRs — straight from the Python code diff. No OpenAPI spec required.
oasdiff needs an OpenAPI spec. buf breaking needs protobuf. Most Python codebases have neither — their public API is the code. Oneport ApiDiff diffs the actual public surface of your changed .py files (via the ast module, deterministically) and gates the merge when a change would break consumers.
$ oneport-apidiff check --staged
oneport-apidiff — --staged
==========================
Verdict: BREAKING (2 breaking, 0 risky, 1 compatible; 1 file(s) checked)
[BREAKING] charge (pkg/payments.py:6)
param `timeout` was removed
- def charge(amount, currency='usd', timeout=30)
+ def charge(amount, currency='usd')
impact: callers passing timeout positionally or by keyword now break
migration: drop the timeout argument; configure timeouts on the client instead
internal callers (2):
app/billing.py:1 from pkg.payments import charge
app/billing.py:14 charge(total, timeout=5)
Exit code 1 on BREAKING — wire it into CI and the merge is blocked until someone consciously overrides.
How it works
Deterministic detection, LLM classification. The model never decides what changed — only what it means.
- Deterministic core (
ast) — base and head versions of every changed.pyfile are compared as public API surfaces: removed symbols, added/removed/renamed/reordered required params, changed defaults, changed type annotations, sync↔async flips, module constants, enum members, dataclass / pydantic-model fields,@propertyaccessors, and__init__.pyre-exports. A public symbol that simply moves between modules is reported as a relocation, not a false remove + add. If anopenapi.yaml/openapi.jsonchanged, its paths/methods/responses are diffed structurally too. Internal callers of every changed symbol are found by a deterministic, usage-aware repo scan (calls, attribute access, and imports — not every line that happens to contain the name) and listed withfile:line. - LLM layer (Claude or Gemini) — each detected change is classified BREAKING / RISKY / COMPATIBLE with a one-line consumer-impact narrative and a migration note. Every change carries a deterministic default verdict, so if the model is down, wrong, or skipped (
--no-llm), the gate still works.
Install
pip install oneport-apidiff
export GEMINI_API_KEY=... # free at https://aistudio.google.com/apikey
# or: export ANTHROPIC_API_KEY=sk-ant-...
Usage
oneport-apidiff check --staged # gate what you're about to commit (default)
oneport-apidiff check --head # gate the last commit
oneport-apidiff check --base main # gate your branch against main
oneport-apidiff check https://github.com/org/repo/pull/42 # gate a PR
oneport-apidiff check https://github.com/org/repo/pull/42 --post # ...and post the verdict
Flags:
| Flag | Effect |
|---|---|
--post |
Post a sticky verdict table + inline comments on the exact changed signatures. Re-runs update in place (hidden marker) instead of duplicating. Requires GITHUB_TOKEN. |
--format inline|json|sarif |
Human, machine, or SARIF 2.1.0 for GitHub code scanning / dashboards. |
--allow-breaking |
Report but exit 0 (adoption mode). |
--no-llm |
Deterministic verdicts only — no API key needed at all. |
Exit codes: 0 clean/compatible · 1 breaking changes · 2 usage/config/auth error.
Waivers
Approve a specific break without opening the gate for everything. Drop a .oneport/apidiff-waivers.yml in the repo:
waivers:
- symbol: core.Client.fetch # exact qualname (or OpenAPI endpoint)
reason: consumers migrated in v3
expires: "2026-12-31" # optional; after it, the gate closes again
kind: symbol_removed # optional; narrow to one change kind
A waived change still appears in the report (marked [WAIVED], and suppressed in SARIF) but no longer fails CI. An expired waiver is ignored automatically, so a temporary exception can't silently hide a break forever.
Team guidelines
ApiDiff reads the same .oneport/guidelines.md file as oneport-review. Verdicts respect your policy:
- internal package, private API — only changes to __all__ symbols are breaking
- CLI flag names are public API too
Detection stays deterministic either way; guidelines only shape what a change means.
CI
See examples/workflows/apidiff.yml. Minimal version:
- run: pip install oneport-apidiff
- run: oneport-apidiff check "${{ github.event.pull_request.html_url }}" --post
env:
GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
Privacy
Serverless by design: your code goes from your machine (or your CI runner) directly to the model API with your own key. No Oneport servers, no telemetry, no storage. See PRIVACY.md.
vs. the alternatives
| oneport-apidiff | oasdiff | buf breaking | |
|---|---|---|---|
| Works without a spec | yes — diffs the code itself | no (OpenAPI only) | no (protobuf only) |
| Python public API (functions/classes/params) | yes | no | no |
| OpenAPI paths/methods/responses | yes (when a spec is present) | yes (deeper) | no |
| Consumer-impact narrative + migration notes | yes (LLM) | no | no |
| Internal caller list ("these files break") | yes | no | no |
Part of the Oneport pre-ship gate suite, alongside oneport-review.
Metadata
Release files for oneport-apidiff 0.2.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| oneport_apidiff-0.2.1.tar.gz | 42.0 kB | Without provenance | Details for oneport_apidiff-0.2.1.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| oneport_apidiff-0.2.1-py3-none-any.whl | Python 3 | none | any | Details for oneport_apidiff-0.2.1-py3-none-any.whl |
Total release size: 89.4 kB
Release files / oneport_apidiff-0.2.1.tar.gz
| Download URL | oneport_apidiff-0.2.1.tar.gz |
|---|---|
| Size | 42.0 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
66bd716cb23ce4a8a9ae42f54e763bacc6082992a41e19f91dba9b50cbfcb726
|
|
BLAKE2b-256 checksum How to use checksums |
fddd8f604cafc105470b9c12aa4e21f77c5640fac5b3b6102daf0dc4cc354dc9
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.0
|
Release files / oneport_apidiff-0.2.1-py3-none-any.whl
| Download URL | oneport_apidiff-0.2.1-py3-none-any.whl |
|---|---|
| Size | 47.4 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
67e225c064c231354f6e25090dc43b8a1ba106c6da7b54587792b32e3756b60d
|
|
BLAKE2b-256 checksum How to use checksums |
909571c67af9072c5b3f0e4453b16f3b0bec5a9ef07eff78213b434d91c04ba0
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.0
|
Security for oneport-apidiff 0.2.1
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.