pryti-contract
Your backend keeps a list of what it does. And it can't lie about it.
Companion library to pryti-semantic-reviewer. Pryti reads code from outside and guesses. This runs inside your app and knows.
What it does
While your app starts, it records:
- every route (path, method, auth)
- every model (fields, types, constraints)
- every outside call (HTTP, email, payments)
- every background job
- how much of that it actually knows vs. couldn't figure out
Then you can ask your app one question and get an answer:
"What does this app actually do?"
Why it matters
AI opens a PR with 400 changed lines. You don't read them. You read this:
RISKY (4)
POST /orders auth: user -> public
POST /orders new effect: net:analytics.example.com
shop.Customer.email unique: True -> False
shop.Customer.name field removed (data loss)
Four lines instead of four hundred.
Install
pip install pryti-contract
Use it — three levels
Level 1: nothing to write
Works on an existing Django project with zero code changes.
pryti-contract export --settings myproject.settings -o contract.json
It reads Django's real router and real model registry. Routes built in loops, DRF routers, mixins — all found, because the app already resolved them at startup.
Coverage will be partial. That's reported, not hidden:
wrote contract.json: 9 routes, 6 models, 0 jobs, auth known on 5/9
Level 2: declare what matters
from pryti_contract import contract
@contract.route("POST /orders", auth="user")
@contract.effects("net:api.stripe.com")
def create_order(request):
...
Eight decorators total, max. AI writes these correctly first try, because they look like every Django decorator it has ever seen.
Level 3: enforce it
# settings.py
MIDDLEWARE = ["pryti_contract.middleware.ContractMiddleware", ...]
# conftest.py or apps.py
from pryti_contract import guard
guard.install(mode="error") # off | record | warn | error
Now an undeclared call fails loudly:
shop.views.leaky_order performed undeclared effect 'net:api.stripe.com'.
Declared: none. Add @contract.effects('net:api.stripe.com') or remove the call.
This is the part no linter can do. The bad code doesn't ship.
Don't know what to declare? Run your tests in record mode and let it tell you:
guard.install(mode="record")
# ... run test suite ...
json.dump(guard.suggestions(), open("observed.json", "w"))
pryti-contract suggest observed.json # prints the decorators to paste in
In CI
- run: pryti-contract export --settings myproject.settings -o head.json
- run: git checkout ${{ github.base_ref }}
- run: pryti-contract export --settings myproject.settings -o base.json
- run: pryti-contract diff base.json head.json --markdown --fail-on risky
--fail-on risky exits 1 on auth weakening, new outside calls, dropped fields,
relaxed uniqueness, or changed relations.
How the effect guard works
One hook at the socket layer, not per-library. requests, httpx, urllib,
boto3, stripe — all covered by the same code, including libraries that
don't exist yet.
socket.getaddrinfois checked before it runs, so a blocked call never leaves the processsocket.socket.connectcatches direct-IP connectionssmtplib.SMTP.sendmailcatches email- localhost and unix sockets are never effects, so your database doesn't trip it
Patterns support wildcards: net:*.stripe.com, net:*, email.
What it does not catch
Worth being blunt about.
- Business logic. If AI changes a discount from 10% to 90%, the contract is identical. Routes same, models same, effects same. Tests catch that; this doesn't.
- Anything the router never sees. Dead code, unmounted views.
- Dynamic hostnames are caught at runtime, not at export time. The contract records what you declared; the guard records what actually happened.
Structural mistakes: this. Logic mistakes: your tests. You need both.
Try it
git clone ... && cd pryti-contract
pip install -e ".[dev]"
pytest
cd examples/demo && pryti-contract export --settings settings --root . -o /tmp/base.json
MIT.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file pryti_contract-0.1.0.tar.gz.
File metadata
- Download URL: pryti_contract-0.1.0.tar.gz
- Upload date:
- Size: 19.6 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/7.0.0 CPython/3.10.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
fc4cc5ce42adfecddb3ebae79cc257d52dfa59958616f48b1e995e577d8906f7
|
|
| MD5 |
08ed1de856eb787ee85bbad3ab5b1216
|
|
| BLAKE2b-256 |
c923f76ad0679f8016608f6164c6373840d7ce5fa5cd3f3f75eb5546241662d0
|
File details
Details for the file pryti_contract-0.1.0-py3-none-any.whl.
File metadata
- Download URL: pryti_contract-0.1.0-py3-none-any.whl
- Upload date:
- Size: 18.3 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/7.0.0 CPython/3.10.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
cf9f3f06bfabe701b11fb1bdd684a7c460660a1e257ff19500859cbb9a0d0404
|
|
| MD5 |
453ee524de94d3c9addd62d77b66ce22
|
|
| BLAKE2b-256 |
a91b52b4bfb0a7e7ec9a5e98530c0289bb5d35e75a420be0904d8b9c887effaa
|