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.1.0.tar.gz (16.6 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.1.0-py3-none-any.whl (13.9 kB view details)

Uploaded Python 3

File details

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

File metadata

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

File hashes

Hashes for snapfix-0.1.0.tar.gz
Algorithm Hash digest
SHA256 99ca0cbe4a2263c3ee7f88bba975a0e4f9230ecce3a315ca11952b982e263e5b
MD5 82e6f772dd021a689be362303bf2fe11
BLAKE2b-256 2108640922f2929df932c0db1b1f8e12245d4a660e6d3c085e9eb42e3f33faa0

See more details on using hashes here.

Provenance

The following attestation bundles were made for snapfix-0.1.0.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.1.0-py3-none-any.whl.

File metadata

  • Download URL: snapfix-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 13.9 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.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 1f38ccdcac9280a3a3df74fbf8f6d41ae8a7201bd3969e38c4200c677e2fd62b
MD5 bf558d35bd01b91bf9323548b06834e4
BLAKE2b-256 107340045b8e29a1f4e869c5cacfe6c36288172892b389714267f12c0ca53511

See more details on using hashes here.

Provenance

The following attestation bundles were made for snapfix-0.1.0-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