Skip to main content

Capture real Python objects, scrub sensitive fields, emit pytest fixtures.

Project description

snapfix

Capture real Python objects from staging, scrub sensitive fields, emit @pytest.fixture files automatically.


The problem

You have a bug that only reproduces with real data. You need a test. You don't want to hand-build a factory that misses the edge case, and you don't want to copy-paste a production payload and accidentally commit a customer's email address.

snapfix solves this with one decorator.


Install

pip install snapfix

Python 3.10+ required. No other required dependencies.


Quickstart

# In your service code (staging or development only)
from snapfix import capture

@capture("invoice_response", scrub=["billing_name"])
def fetch_invoice(invoice_id: str) -> dict:
    return external_api.get(f"/invoices/{invoice_id}")

Call the function once against staging. snapfix writes this to tests/fixtures/snapfix_invoice_response.py:

# Generated by snapfix -- do not edit manually
# Captured : 2026-03-23T14:22:01
# Scrubbed : customer.email, meta.token, billing_name
import pytest
from snapfix import reconstruct

@pytest.fixture
def invoice_response():
    return reconstruct({
        "id": "INV-8821",
        "customer": {
            "email": "***SCRUBBED***",
            "billing_name": "***SCRUBBED***",
            "plan": "pro",
        },
        "amount": {"__snapfix_type__": "decimal", "value": "149.99"},
        "issued_at": {"__snapfix_type__": "datetime", "value": "2026-03-01T09:00:00"},
        "meta": {
            "token": "***SCRUBBED***",
            "retry": 0,
        },
    })

Use it in any test:

def test_invoice_total(invoice_response):
    assert invoice_response["amount"].quantize(Decimal("0.01")) == Decimal("149.99")

That's it. No factory definition. No manual scrubbing. No pasted JSON.


PII scrubbing

⚠ Important limitation: snapfix scrubs fields by key name only. It does NOT detect PII in field values. An email address stored as response["tags"][0] or inside a list will not be scrubbed. Always review generated fixtures before committing them.

Default scrubbed field names

Any key whose name contains one of these strings (case-insensitive, substring match) is scrubbed:

email · password · passwd · token · secret · api_key · apikey · access_token · refresh_token · ssn · credit_card · card_number · cvv · phone · mobile · dob · date_of_birth · address · ip_address · authorization · auth · bearer

customer_email → scrubbed (substring match on email)
billing_phone_number → scrubbed (substring match on phone)
retry_countnot scrubbed

Adding custom fields

@capture("order", scrub=["customer_id", "tax_number"])
def fetch_order(order_id: str) -> dict: ...

Replacement values

Field value type Replacement
str "***SCRUBBED***"
int / float -1
None "***SCRUBBED***"

Supported types

snapfix serializes the following Python types and restores them correctly via reconstruct():

Type Serialized as Restored on reconstruct()
dict, list, str, int, float, bool, None JSON native Same
datetime.datetime ISO 8601 string + marker datetime
datetime.date ISO 8601 string + marker date
datetime.time ISO 8601 string + marker time
datetime.timedelta total seconds + marker timedelta
uuid.UUID string + marker UUID
decimal.Decimal string + marker Decimal
bytes base64 + marker bytes
bytearray base64 + marker bytearray
pathlib.Path string + marker Path
enum.Enum .value + marker value only (enum class not preserved)
tuple list + marker list (documented: tuples become lists)
set / frozenset sorted list + marker set / frozenset
dataclass dataclasses.asdict() then recurse dict
pydantic.BaseModel .model_dump() then recurse dict
Circular reference {"__snapfix_circular__": true} sentinel dict
Unserializable type {"__snapfix_unserializable__": true, ...} sentinel dict

Note: tuplelist on roundtrip is intentional. JSON has no tuple type. If your tests depend on the exact type, assert isinstance(result, list) rather than tuple.


Configuration

Environment variables

Variable Default Description
SNAPFIX_OUTPUT_DIR tests/fixtures Where fixture files are written
SNAPFIX_MAX_DEPTH 10 Maximum serialization depth
SNAPFIX_MAX_SIZE 500000 Maximum payload size in bytes
SNAPFIX_ENABLED true Set to false to disable all capture

snapfix.yaml (project root)

snapfix:
  output_dir: tests/fixtures
  max_depth: 10
  max_size_bytes: 500000
  enabled: true

Priority order (highest to lowest)

  1. Decorator parameters: @capture(name, scrub=[...], max_depth=5)
  2. Environment variables
  3. snapfix.yaml
  4. Built-in defaults

Disabling in production

Set SNAPFIX_ENABLED=false in your production environment. The decorator becomes a no-op — zero overhead, no files written, no exceptions swallowed.


CLI

snapfix list              # List all captured fixtures with metadata
snapfix show  <name>      # Print a fixture to stdout
snapfix clear <name>      # Delete one fixture (prompts for confirmation)
snapfix clear-all         # Delete all fixtures (prompts for confirmation)

All commands accept --dir <path> to target a non-default fixture directory.


Decorator reference

@capture(
    name,                # str — fixture name; becomes the function name in the output file
    scrub=None,          # list[str] | None — extra field names to scrub (merged with defaults)
    max_depth=None,      # int | None — override max serialization depth
    max_size_bytes=None, # int | None — override max payload size
    config=None,         # SnapfixConfig | None — full config override
)

Works on both sync and async functions. The decorator is transparent:

  • The return value is always preserved unchanged.
  • If the wrapped function raises, the exception propagates normally and no file is written.
  • If serialization fails for any reason, a warnings.warn() is emitted and execution continues.

FAQ

Is it safe to use against production traffic? No. Use snapfix against staging or a development environment. Production traffic contains real customer data. Even with scrubbing enabled, value-level PII (email addresses in list fields, etc.) will not be caught.

Does it slow down my application? Serialization adds latency proportional to payload size. For typical API responses (< 50 KB), the overhead is negligible in staging. Do not enable SNAPFIX_ENABLED=true in a production critical path.

What happens if the object is too large? If the serialized payload exceeds max_size_bytes, the fixture is not written and a warning is emitted. Increase SNAPFIX_MAX_SIZE or add depth limiting with max_depth.

What happens if a field contains an unserializable type? It is replaced with a sentinel dict: {"__snapfix_unserializable__": true, "__snapfix_repr__": "...", "__snapfix_type_name__": "..."}. The rest of the object is still captured. reconstruct() returns the sentinel as-is.

Can I regenerate a fixture? Yes. Re-run the decorated function. The fixture file is overwritten in place.

Does it work with pytest parametrize? The generated file is a standard @pytest.fixture. It works with everything pytest supports.


Development

git clone https://github.com/yourname/snapfix
cd snapfix
pip install -e ".[dev]"
pytest
ruff check src/

License

MIT

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

snapfix-0.3.1.tar.gz (43.0 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

snapfix-0.3.1-py3-none-any.whl (28.2 kB view details)

Uploaded Python 3

File details

Details for the file snapfix-0.3.1.tar.gz.

File metadata

  • Download URL: snapfix-0.3.1.tar.gz
  • Upload date:
  • Size: 43.0 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.7

File hashes

Hashes for snapfix-0.3.1.tar.gz
Algorithm Hash digest
SHA256 dc9f4c6081b34925dcbbd1bcee18d9edf15ecf3f450a361193b3a44bba68132e
MD5 3ba59431343a1e4dce36ccddb59068bb
BLAKE2b-256 c8b4d1facde1987eff70670123c60d54e6abbd9a6ff7189704ebe1b2a55f2537

See more details on using hashes here.

Provenance

The following attestation bundles were made for snapfix-0.3.1.tar.gz:

Publisher: release.yml on hacky1997/snapfix

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file snapfix-0.3.1-py3-none-any.whl.

File metadata

  • Download URL: snapfix-0.3.1-py3-none-any.whl
  • Upload date:
  • Size: 28.2 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.7

File hashes

Hashes for snapfix-0.3.1-py3-none-any.whl
Algorithm Hash digest
SHA256 9bbd393c753dcb151005ddf9481cec75985aaa28b6ea500cc2be776d7160d7b5
MD5 674eed4bfbe168e6ac91119401f13d04
BLAKE2b-256 1b7caa5ccea4efd25b1a3f79ef6fe28301b9b06f8f651dda58d001bbf6d02d98

See more details on using hashes here.

Provenance

The following attestation bundles were made for snapfix-0.3.1-py3-none-any.whl:

Publisher: release.yml on hacky1997/snapfix

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page