StubSmith Python SDK
Instrument outbound HTTP calls made by your Python application and forward them, request and response, to the StubSmith ingest service for capture, anonymization, and replay.
Supports both requests and httpx (sync and async). Sending is
non-blocking and fire-and-forget: a background daemon thread drains a bounded
queue; any failure (network, serialization, queue overflow) is silently
discarded and never propagates to your application.
Anonymization / masking is applied server-side by the Go ingest service
(ingest-go) - do not pre-mask data client-side.
Install
# with requests support
pip install "stubsmith[requests]"
# with httpx support
pip install "stubsmith[httpx]"
# both
pip install "stubsmith[requests,httpx]"
Quickstart
import stubsmith
# One-liner: instruments both requests and httpx (whichever is importable)
client = stubsmith.install(api_key="sk-your-project-key")
# Now every outbound call is captured automatically:
import requests
resp = requests.get("https://api.stripe.com/v1/charges")
import httpx
resp = httpx.get("https://api.example.com/users")
Or configure via environment variables and call install() with no arguments:
export STUBSMITH_URL="http://ingest:8081/v1/captures"
export STUBSMITH_API_KEY="sk-your-project-key"
import stubsmith
stubsmith.install()
Selective instrumentation
from stubsmith import StubSmith
client = StubSmith(url="...", api_key="sk-...")
client.instrument_requests() # only requests
# client.instrument_httpx() # only httpx
# Remove patches later:
client.uninstrument()
Configuration
| Parameter | Default | Description |
|---|---|---|
url |
$STUBSMITH_URL / https://ingest.stubsmith.dev/v1/captures |
Full URL of the ingest endpoint |
api_key |
$STUBSMITH_API_KEY |
Bearer token; empty value auto-disables the client |
enabled |
True |
Master switch (also auto-disabled when api_key is absent) |
timeout |
5 (seconds) |
HTTP timeout for ingest POST |
max_body_bytes |
65536 (64 KiB) |
Truncate captured bodies to this size before sending |
sample_rate |
1.0 |
Fraction of calls to forward (0.0 - 1.0) |
queue_maxsize |
1000 |
Bound on the background queue; excess items are dropped |
flush_timeout |
$STUBSMITH_FLUSH_TIMEOUT / 1.0 (seconds) |
How long process exit waits for queued captures to drain. 0 disables the wait |
An outage cannot block your application
Captures are masked on the calling thread (CPU only, no I/O) and handed to a
bounded queue with a non-blocking put. A daemon thread does the HTTP POST and
swallows every exception. If the ingest service is slow, unreachable or
returning errors, your request path is unaffected: the queue fills, excess
captures are dropped, and nothing propagates to your code.
Measured with the ingest host blackholed, so every POST runs to its timeout: a median of 0.32 ms and a p95 of 0.45 ms added to an instrumented call.
The one place an outage is visible is process exit, where an atexit hook waits
for the queue to drain. That wait is capped at flush_timeout, and is abandoned
as soon as a send fails, so an unreachable endpoint costs about a second rather
than the full budget. Set STUBSMITH_FLUSH_TIMEOUT=0 in a serverless function or
anywhere else exit latency is billed; captures still in the queue are discarded.
How it works
install()/instrument_requests()/instrument_httpx()monkey-patches the relevant HTTP client (idempotent; safe to call multiple times).- Each call is timed; request headers/body and response status/headers/body are captured without consuming streams - if you opened a streaming response the SDK skips the body rather than interfering.
- Captures are placed on an in-process
queue.Queue; a daemon thread drains it and POSTs toPOST /v1/captureswithAuthorization: Bearer <api_key>. - On process exit an
atexithandler flushes the queue (bounded timeout).
Fingerprint value discrimination
By default the SDK fingerprints each request by its body structure (key-paths),
query parameter names, and Content-Type. Endpoints that multiplex operations via a
body field (e.g. {"action": "login"} vs {"action": "delete_user"}) therefore
produce a single fingerprint, which means a single review and a single privacy-rule
set for both variants.
Enable value discrimination on action in the StubSmith UI or via the API and the
SDK will automatically include that field's value in the hash:
# No SDK change required - configure via the UI or API, then the SDK picks up the
# new value paths on its next rules-sync poll (default: every 60 seconds).
import stubsmith
stubsmith.install(url="...", api_key="sk-...")
import requests
requests.post("https://api.example.com/rpc", json={"action": "login", "username": "alice"})
# → fingerprint A (action=login)
requests.post("https://api.example.com/rpc", json={"action": "delete_user", "user_id": 42})
# → fingerprint B (action=delete_user) - separate review, separate rules
See docs/fingerprint-value-discrimination.md
for a full walkthrough including the hash mechanics, all three configuration methods,
and the privacy guarantees.
Offline replay in tests
stubsmith.replay() serves recorded responses to your code's outbound HTTP
calls. Inside the block no network call is made and the dependency does not
need to be running.
import stubsmith
def test_charge_is_declined():
with stubsmith.replay():
with pytest.raises(CardDeclined):
PaymentClient().charge(amount_cents=950_000, currency="USD")
Fetch the bundle once and commit it:
export STUBSMITH_API_KEY=<your project key>
stubsmith pull --out .stubsmith/bundle.json
python -m stubsmith pull does the same thing. It resolves the package from
sys.path, so it runs a checkout without installing it, and it cannot pick up a
stale installed copy in place of the one you are working on.
From then on the test suite needs no key, no network and no pull step - it
reads the committed file. Refresh it when the recording should change: the
upstream API's shape changed, you added a call the bundle does not cover, or you
approved new fingerprints. Treat it like a lockfile or a golden file: an
occasional, reviewed, committed change.
replay() finds the bundle without configuration - an explicit path, then
$STUBSMITH_BUNDLE, then an upward search from the working directory that stops
at the first directory containing .git or pyproject.toml. Pass a path or a
dict to be explicit:
with stubsmith.replay("tests/data/bundle.json"):
...
Matching
A request is matched on (domain, method, path_template, fingerprint). The
fingerprint covers body key-paths, query parameter names and the normalised
content-type - not values, and not the host or path, which is why the other
three parts of the key are needed. Every body-less GET shares one
fingerprint.
Dynamic path segments are templated from the recording, so /api/users/4821
matches a stub recorded as /api/users/{id}.
When nothing matches
StubNotFound is raised with a diff of what was sent against the closest
recording, naming the fields that differ. It never falls through to the
network, so a test cannot silently start calling a real service.
A stub whose fingerprint has no recorded captures is reported as degraded by
stubsmith pull and raises the same error at replay time, rather than serving an
empty response.
See examples/fixtures-testing/ for a complete worked example: a real service, a
client instrumented with the SDK, traffic captured and reviewed, and a test suite
that passes with the service stopped.
Single-fixture helpers
stubsmith.testing handles individual fixture files rather than a whole bundle,
for cases where you want one recorded exchange registered against responses:
pip install "stubsmith[testing]"
from stubsmith import testing
bundle = testing.load_bundle("fixtures/get_user.json")
testing.register_template(responses, bundle, base_url="http://api")
It also provides assert_request_matches_fixture and
assert_body_schemas_match as contract guards. For a normal test suite
replay() is the simpler path - it covers every recorded endpoint at once and
needs no per-fixture registration.
Masking and placeholders
Values are masked in the SDK, before a capture is uploaded: the server never
receives the originals. A field with no keep rule is replaced.
By default the replacement is a constant ("<masked>", 0, False) matching
the original's type.
Setting STUBSMITH_MASK_SALT to any non-empty string switches on
format-preserving placeholders for fields carrying a semantic type hint
(email, uuid, iso8601, e164, iban, url, decimal_amount,
integer_id, opaque_token, free_text). The replacement then has the same
shape as the original - a parseable timestamp, an RFC 4122 UUID, an IBAN with a
correct mod-97 checksum - so code that parses or validates these values still
works against a recording. The same salt and value always produce the same
placeholder, which preserves uniqueness and cross-field references. The salt
never leaves the process.
Two types are never format-preserved, regardless of salt: currency_code and
country_code. Booleans are refused for the same reason. Their domains are
small enough that a keyed hash could be reversed with a lookup table. Use
action: keep for those fields instead - they are rarely sensitive.
Development / tests
python3 -m venv .venv && . .venv/bin/activate
pip install -e '.[test]'
python -m pytest -q
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 stubsmith-0.1.0.tar.gz.
File metadata
- Download URL: stubsmith-0.1.0.tar.gz
- Upload date:
- Size: 148.0 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
b60acd2bd88906bf31bb62d84e4393ae179fb8965fbe2de17d4e74c2281bc250
|
|
| MD5 |
4c08064016f81e2921da1071e92b9cce
|
|
| BLAKE2b-256 |
9144f3d10636254ec8d8f87268d99e1cd6db5a86a223306436a9ab8476ee0a50
|
Provenance
The following attestation bundles were made for stubsmith-0.1.0.tar.gz:
Publisher:
release.yml on Stubsmith/stubsmith-python
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
stubsmith-0.1.0.tar.gz -
Subject digest:
b60acd2bd88906bf31bb62d84e4393ae179fb8965fbe2de17d4e74c2281bc250 - Sigstore transparency entry: 2614665980
- Sigstore integration time:
-
Permalink:
Stubsmith/stubsmith-python@0ebef583cd68c04640944999fd1c103b5351a59a -
Branch / Tag:
refs/heads/main - Owner: https://github.com/Stubsmith
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@0ebef583cd68c04640944999fd1c103b5351a59a -
Trigger Event:
workflow_dispatch
-
Statement type:
File details
Details for the file stubsmith-0.1.0-py3-none-any.whl.
File metadata
- Download URL: stubsmith-0.1.0-py3-none-any.whl
- Upload date:
- Size: 86.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
faabad3b3c39367609a4fc7726b0b76b235655f550c020254d345ed1820aed92
|
|
| MD5 |
cb7f1e969e26a489c209fd212da76467
|
|
| BLAKE2b-256 |
fa48929f92a75c787048dfc620555ce60e638ca42e680502533562afa14cd3cd
|
Provenance
The following attestation bundles were made for stubsmith-0.1.0-py3-none-any.whl:
Publisher:
release.yml on Stubsmith/stubsmith-python
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
stubsmith-0.1.0-py3-none-any.whl -
Subject digest:
faabad3b3c39367609a4fc7726b0b76b235655f550c020254d345ed1820aed92 - Sigstore transparency entry: 2614666003
- Sigstore integration time:
-
Permalink:
Stubsmith/stubsmith-python@0ebef583cd68c04640944999fd1c103b5351a59a -
Branch / Tag:
refs/heads/main - Owner: https://github.com/Stubsmith
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@0ebef583cd68c04640944999fd1c103b5351a59a -
Trigger Event:
workflow_dispatch
-
Statement type: