snapfix 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- snapfix-0.1.0/.github/workflows/ci.yml +17 -0
- snapfix-0.1.0/.github/workflows/release.yml +43 -0
- snapfix-0.1.0/.gitignore +57 -0
- snapfix-0.1.0/CHANGELOG.md +45 -0
- snapfix-0.1.0/CONTRIBUTING.md +79 -0
- snapfix-0.1.0/LICENSE +21 -0
- snapfix-0.1.0/PKG-INFO +275 -0
- snapfix-0.1.0/README.md +242 -0
- snapfix-0.1.0/pyproject.toml +67 -0
- snapfix-0.1.0/snapfix.yaml.example +20 -0
- snapfix-0.1.0/src/snapfix/__init__.py +6 -0
- snapfix-0.1.0/src/snapfix/capture.py +84 -0
- snapfix-0.1.0/src/snapfix/cli.py +97 -0
- snapfix-0.1.0/src/snapfix/codegen.py +77 -0
- snapfix-0.1.0/src/snapfix/config.py +51 -0
- snapfix-0.1.0/src/snapfix/py.typed +0 -0
- snapfix-0.1.0/src/snapfix/reconstruct.py +8 -0
- snapfix-0.1.0/src/snapfix/scrubber.py +49 -0
- snapfix-0.1.0/src/snapfix/serializer.py +166 -0
- snapfix-0.1.0/src/snapfix/store.py +57 -0
- snapfix-0.1.0/tests/__init__.py +0 -0
- snapfix-0.1.0/tests/conftest.py +8 -0
- snapfix-0.1.0/tests/test_snapfix.py +451 -0
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
on: [push, pull_request]
|
|
3
|
+
|
|
4
|
+
jobs:
|
|
5
|
+
test:
|
|
6
|
+
strategy:
|
|
7
|
+
matrix:
|
|
8
|
+
python-version: ["3.10", "3.11", "3.12"]
|
|
9
|
+
runs-on: ubuntu-latest
|
|
10
|
+
steps:
|
|
11
|
+
- uses: actions/checkout@v4
|
|
12
|
+
- uses: actions/setup-python@v5
|
|
13
|
+
with:
|
|
14
|
+
python-version: ${{ matrix.python-version }}
|
|
15
|
+
- run: pip install -e ".[dev]"
|
|
16
|
+
- run: pytest --tb=short -q
|
|
17
|
+
- run: ruff check src/
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
name: Release
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags:
|
|
6
|
+
- "v*"
|
|
7
|
+
|
|
8
|
+
permissions:
|
|
9
|
+
contents: write # for creating GitHub Release
|
|
10
|
+
id-token: write # for PyPI trusted publishing (OIDC)
|
|
11
|
+
|
|
12
|
+
jobs:
|
|
13
|
+
release:
|
|
14
|
+
runs-on: ubuntu-latest
|
|
15
|
+
environment: pypi # matches the trusted publisher environment on PyPI
|
|
16
|
+
|
|
17
|
+
steps:
|
|
18
|
+
- uses: actions/checkout@v4
|
|
19
|
+
|
|
20
|
+
- uses: actions/setup-python@v5
|
|
21
|
+
with:
|
|
22
|
+
python-version: "3.12"
|
|
23
|
+
|
|
24
|
+
- name: Install hatch
|
|
25
|
+
run: pip install hatch
|
|
26
|
+
|
|
27
|
+
- name: Run tests before releasing
|
|
28
|
+
run: |
|
|
29
|
+
pip install -e ".[dev]"
|
|
30
|
+
pytest --tb=short -q
|
|
31
|
+
|
|
32
|
+
- name: Build distributions
|
|
33
|
+
run: hatch build
|
|
34
|
+
|
|
35
|
+
- name: Create GitHub Release
|
|
36
|
+
uses: softprops/action-gh-release@v2
|
|
37
|
+
with:
|
|
38
|
+
files: dist/*
|
|
39
|
+
generate_release_notes: true
|
|
40
|
+
|
|
41
|
+
- name: Publish to PyPI
|
|
42
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
43
|
+
# No api_token needed — uses OIDC trusted publishing
|
snapfix-0.1.0/.gitignore
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*.pyo
|
|
5
|
+
*.pyd
|
|
6
|
+
.Python
|
|
7
|
+
*.egg
|
|
8
|
+
*.egg-info/
|
|
9
|
+
dist/
|
|
10
|
+
build/
|
|
11
|
+
eggs/
|
|
12
|
+
.eggs/
|
|
13
|
+
lib/
|
|
14
|
+
lib64/
|
|
15
|
+
parts/
|
|
16
|
+
sdist/
|
|
17
|
+
var/
|
|
18
|
+
wheels/
|
|
19
|
+
*.whl
|
|
20
|
+
.installed.cfg
|
|
21
|
+
MANIFEST
|
|
22
|
+
|
|
23
|
+
# Virtual environments
|
|
24
|
+
.venv/
|
|
25
|
+
venv/
|
|
26
|
+
env/
|
|
27
|
+
ENV/
|
|
28
|
+
|
|
29
|
+
# Testing
|
|
30
|
+
.pytest_cache/
|
|
31
|
+
.hypothesis/
|
|
32
|
+
.coverage
|
|
33
|
+
coverage.xml
|
|
34
|
+
htmlcov/
|
|
35
|
+
*.coveragerc
|
|
36
|
+
junit.xml
|
|
37
|
+
|
|
38
|
+
# Type checking
|
|
39
|
+
.mypy_cache/
|
|
40
|
+
.dmypy.json
|
|
41
|
+
dmypy.json
|
|
42
|
+
.pytype/
|
|
43
|
+
|
|
44
|
+
# Editors
|
|
45
|
+
.vscode/
|
|
46
|
+
.idea/
|
|
47
|
+
*.swp
|
|
48
|
+
*.swo
|
|
49
|
+
*~
|
|
50
|
+
.DS_Store
|
|
51
|
+
|
|
52
|
+
# snapfix runtime output (do NOT ignore tests/fixtures — those are committed)
|
|
53
|
+
.snapfix_index.json
|
|
54
|
+
snapfix.yaml.bak
|
|
55
|
+
|
|
56
|
+
# hatch
|
|
57
|
+
.hatch/
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to snapfix are documented here.
|
|
4
|
+
|
|
5
|
+
The format follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
|
6
|
+
snapfix uses [Semantic Versioning](https://semver.org/).
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## [Unreleased]
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## [0.1.0] — 2026-03-23
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
- `@capture` decorator — captures return values of sync and async functions
|
|
18
|
+
- `reconstruct()` — restores `__snapfix_type__` markers to Python types
|
|
19
|
+
- `SnapfixConfig` — config dataclass with env var and YAML override hierarchy
|
|
20
|
+
- `SnapfixSerializer` — recursive serializer supporting 15 Python types:
|
|
21
|
+
`datetime`, `date`, `time`, `timedelta`, `UUID`, `Decimal`, `bytes`,
|
|
22
|
+
`bytearray`, `Path`, `Enum`, `set`, `frozenset`, `tuple`, `dataclass`,
|
|
23
|
+
pydantic v1/v2 `BaseModel`
|
|
24
|
+
- Circular reference detection — emits `__snapfix_circular__` sentinel
|
|
25
|
+
- Max depth guard — emits `__snapfix_maxdepth__` sentinel at `max_depth`
|
|
26
|
+
- Max size guard — emits `__snapfix_truncated__` sentinel above `max_size_bytes`
|
|
27
|
+
- Unserializable type fallback — emits `__snapfix_unserializable__` sentinel
|
|
28
|
+
- `SnapfixScrubber` — recursive field-name-based PII scrubber (case-insensitive
|
|
29
|
+
substring matching, 22 default field patterns, no input mutation)
|
|
30
|
+
- `SnapfixCodegen` — generates valid `@pytest.fixture` Python source
|
|
31
|
+
- `SnapfixStore` — atomic fixture file writes with `.snapfix_index.json`
|
|
32
|
+
- CLI: `snapfix list`, `snapfix show`, `snapfix clear`, `snapfix clear-all`
|
|
33
|
+
- `snapfix.yaml` project-level configuration support
|
|
34
|
+
- `SNAPFIX_ENABLED`, `SNAPFIX_OUTPUT_DIR`, `SNAPFIX_MAX_DEPTH`,
|
|
35
|
+
`SNAPFIX_MAX_SIZE` environment variable overrides
|
|
36
|
+
|
|
37
|
+
### Documented limitations
|
|
38
|
+
- Field-name scrubbing only: PII in field values is not detected
|
|
39
|
+
- `tuple` → `list` on roundtrip (JSON has no tuple type)
|
|
40
|
+
- Enum class is not preserved on roundtrip, only `.value`
|
|
41
|
+
- Objects without `__dict__`, `.model_dump()`, or `.dict()` emit the
|
|
42
|
+
`__snapfix_unserializable__` sentinel
|
|
43
|
+
|
|
44
|
+
[Unreleased]: https://github.com/yourname/snapfix/compare/v0.1.0...HEAD
|
|
45
|
+
[0.1.0]: https://github.com/yourname/snapfix/releases/tag/v0.1.0
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# Contributing to snapfix
|
|
2
|
+
|
|
3
|
+
Thank you for your interest. Please read this before opening a PR.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Scope
|
|
8
|
+
|
|
9
|
+
snapfix does exactly three things: capture Python objects, scrub PII by field name,
|
|
10
|
+
and emit `@pytest.fixture` files. PRs that expand this scope will be declined.
|
|
11
|
+
|
|
12
|
+
**Explicitly out of scope for snapfix:**
|
|
13
|
+
- Database row capture (separate tool)
|
|
14
|
+
- HTTP request/response recording (use vcrpy)
|
|
15
|
+
- NLP-based or value-level PII detection
|
|
16
|
+
- Binary serialization formats (msgpack, cbor, pickle)
|
|
17
|
+
- Snapshot diffing or regression detection
|
|
18
|
+
- Web UI or dashboards
|
|
19
|
+
- Support for Python < 3.10
|
|
20
|
+
|
|
21
|
+
If you have an idea that does not fit snapfix's scope, consider opening a
|
|
22
|
+
discussion before writing code.
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## Development setup
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
git clone https://github.com/yourname/snapfix
|
|
30
|
+
cd snapfix
|
|
31
|
+
pip install -e ".[dev]"
|
|
32
|
+
pytest
|
|
33
|
+
ruff check src/
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## Before opening a PR
|
|
39
|
+
|
|
40
|
+
1. **Tests are required.** Every change to `src/snapfix/` must be covered by a
|
|
41
|
+
corresponding test in `tests/test_snapfix.py`. PRs without tests will not be
|
|
42
|
+
reviewed.
|
|
43
|
+
|
|
44
|
+
2. **The serialization contract is public.** The `__snapfix_type__` marker format
|
|
45
|
+
in `serializer.py` is a public API from v1.0.0 onwards. Changes that alter
|
|
46
|
+
the format of existing markers are breaking changes and require a major version bump.
|
|
47
|
+
|
|
48
|
+
3. **The scrubber must not mutate inputs.** `SnapfixScrubber.scrub()` must always
|
|
49
|
+
return a deep copy. Tests enforce this. Do not change this behaviour.
|
|
50
|
+
|
|
51
|
+
4. **Exception safety in `capture`.** The `_record()` function in `capture.py`
|
|
52
|
+
must never raise. Failures emit `warnings.warn()` and return silently.
|
|
53
|
+
Do not remove this guarantee.
|
|
54
|
+
|
|
55
|
+
5. **Run `ruff check src/` and `mypy src/snapfix/` before submitting.**
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
## Reporting bugs
|
|
60
|
+
|
|
61
|
+
Open a GitHub issue with:
|
|
62
|
+
- Python version
|
|
63
|
+
- snapfix version (`pip show snapfix`)
|
|
64
|
+
- Minimal reproducer (object type, decorator usage)
|
|
65
|
+
- Actual vs. expected behaviour
|
|
66
|
+
|
|
67
|
+
Do not include real production data in bug reports.
|
|
68
|
+
|
|
69
|
+
---
|
|
70
|
+
|
|
71
|
+
## PII scrubbing PRs
|
|
72
|
+
|
|
73
|
+
PRs that add new default scrub fields are welcome if the field name pattern is
|
|
74
|
+
unambiguous and broadly applicable. PRs that change substring matching behaviour
|
|
75
|
+
to exact matching are breaking changes.
|
|
76
|
+
|
|
77
|
+
PRs adding value-level PII detection (e.g. regex scanning of string values) are
|
|
78
|
+
out of scope for the core package. They belong in an optional integration
|
|
79
|
+
(`pip install snapfix[presidio]`), which is a future roadmap item.
|
snapfix-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 snapfix contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
snapfix-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: snapfix
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Capture real Python objects, scrub sensitive fields, emit pytest fixtures.
|
|
5
|
+
Project-URL: Homepage, https://github.com/yourname/snapfix
|
|
6
|
+
Project-URL: Documentation, https://github.com/yourname/snapfix#readme
|
|
7
|
+
Project-URL: Issues, https://github.com/yourname/snapfix/issues
|
|
8
|
+
Project-URL: Changelog, https://github.com/yourname/snapfix/blob/main/CHANGELOG.md
|
|
9
|
+
License: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: capture,fixtures,pii,pytest,scrubbing,testing
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
20
|
+
Classifier: Topic :: Software Development :: Testing
|
|
21
|
+
Classifier: Typing :: Typed
|
|
22
|
+
Requires-Python: >=3.10
|
|
23
|
+
Requires-Dist: pyyaml>=6.0
|
|
24
|
+
Requires-Dist: typer>=0.12
|
|
25
|
+
Provides-Extra: dev
|
|
26
|
+
Requires-Dist: hypothesis>=6.0; extra == 'dev'
|
|
27
|
+
Requires-Dist: mypy>=1.0; extra == 'dev'
|
|
28
|
+
Requires-Dist: pydantic>=2.0; extra == 'dev'
|
|
29
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
|
|
30
|
+
Requires-Dist: pytest>=7.0; extra == 'dev'
|
|
31
|
+
Requires-Dist: ruff>=0.4; extra == 'dev'
|
|
32
|
+
Description-Content-Type: text/markdown
|
|
33
|
+
|
|
34
|
+
# snapfix
|
|
35
|
+
|
|
36
|
+
**Capture real Python objects from staging, scrub sensitive fields, emit `@pytest.fixture` files automatically.**
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## The problem
|
|
41
|
+
|
|
42
|
+
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.
|
|
43
|
+
|
|
44
|
+
snapfix solves this with one decorator.
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
## Install
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
pip install snapfix
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Python 3.10+ required. No other required dependencies.
|
|
55
|
+
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
## Quickstart
|
|
59
|
+
|
|
60
|
+
```python
|
|
61
|
+
# In your service code (staging or development only)
|
|
62
|
+
from snapfix import capture
|
|
63
|
+
|
|
64
|
+
@capture("invoice_response", scrub=["billing_name"])
|
|
65
|
+
def fetch_invoice(invoice_id: str) -> dict:
|
|
66
|
+
return external_api.get(f"/invoices/{invoice_id}")
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Call the function once against staging. snapfix writes this to `tests/fixtures/snapfix_invoice_response.py`:
|
|
70
|
+
|
|
71
|
+
```python
|
|
72
|
+
# Generated by snapfix -- do not edit manually
|
|
73
|
+
# Captured : 2026-03-23T14:22:01
|
|
74
|
+
# Scrubbed : customer.email, meta.token, billing_name
|
|
75
|
+
import pytest
|
|
76
|
+
from snapfix import reconstruct
|
|
77
|
+
|
|
78
|
+
@pytest.fixture
|
|
79
|
+
def invoice_response():
|
|
80
|
+
return reconstruct({
|
|
81
|
+
"id": "INV-8821",
|
|
82
|
+
"customer": {
|
|
83
|
+
"email": "***SCRUBBED***",
|
|
84
|
+
"billing_name": "***SCRUBBED***",
|
|
85
|
+
"plan": "pro",
|
|
86
|
+
},
|
|
87
|
+
"amount": {"__snapfix_type__": "decimal", "value": "149.99"},
|
|
88
|
+
"issued_at": {"__snapfix_type__": "datetime", "value": "2026-03-01T09:00:00"},
|
|
89
|
+
"meta": {
|
|
90
|
+
"token": "***SCRUBBED***",
|
|
91
|
+
"retry": 0,
|
|
92
|
+
},
|
|
93
|
+
})
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Use it in any test:
|
|
97
|
+
|
|
98
|
+
```python
|
|
99
|
+
def test_invoice_total(invoice_response):
|
|
100
|
+
assert invoice_response["amount"].quantize(Decimal("0.01")) == Decimal("149.99")
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
That's it. No factory definition. No manual scrubbing. No pasted JSON.
|
|
104
|
+
|
|
105
|
+
---
|
|
106
|
+
|
|
107
|
+
## PII scrubbing
|
|
108
|
+
|
|
109
|
+
> **⚠ Important limitation:** snapfix scrubs fields by **key name only**. It does NOT detect PII
|
|
110
|
+
> in field values. An email address stored as `response["tags"][0]` or inside a list
|
|
111
|
+
> will **not** be scrubbed. Always review generated fixtures before committing them.
|
|
112
|
+
|
|
113
|
+
### Default scrubbed field names
|
|
114
|
+
|
|
115
|
+
Any key whose name contains one of these strings (case-insensitive, substring match) is scrubbed:
|
|
116
|
+
|
|
117
|
+
`email` · `password` · `passwd` · `token` · `secret` · `api_key` · `apikey` ·
|
|
118
|
+
`access_token` · `refresh_token` · `ssn` · `credit_card` · `card_number` ·
|
|
119
|
+
`cvv` · `phone` · `mobile` · `dob` · `date_of_birth` · `address` ·
|
|
120
|
+
`ip_address` · `authorization` · `auth` · `bearer`
|
|
121
|
+
|
|
122
|
+
`customer_email` → scrubbed (substring match on `email`)
|
|
123
|
+
`billing_phone_number` → scrubbed (substring match on `phone`)
|
|
124
|
+
`retry_count` → **not** scrubbed
|
|
125
|
+
|
|
126
|
+
### Adding custom fields
|
|
127
|
+
|
|
128
|
+
```python
|
|
129
|
+
@capture("order", scrub=["customer_id", "tax_number"])
|
|
130
|
+
def fetch_order(order_id: str) -> dict: ...
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
### Replacement values
|
|
134
|
+
|
|
135
|
+
| Field value type | Replacement |
|
|
136
|
+
|---|---|
|
|
137
|
+
| `str` | `"***SCRUBBED***"` |
|
|
138
|
+
| `int` / `float` | `-1` |
|
|
139
|
+
| `None` | `"***SCRUBBED***"` |
|
|
140
|
+
|
|
141
|
+
---
|
|
142
|
+
|
|
143
|
+
## Supported types
|
|
144
|
+
|
|
145
|
+
snapfix serializes the following Python types and restores them correctly via `reconstruct()`:
|
|
146
|
+
|
|
147
|
+
| Type | Serialized as | Restored on `reconstruct()` |
|
|
148
|
+
|---|---|---|
|
|
149
|
+
| `dict`, `list`, `str`, `int`, `float`, `bool`, `None` | JSON native | Same |
|
|
150
|
+
| `datetime.datetime` | ISO 8601 string + marker | `datetime` |
|
|
151
|
+
| `datetime.date` | ISO 8601 string + marker | `date` |
|
|
152
|
+
| `datetime.time` | ISO 8601 string + marker | `time` |
|
|
153
|
+
| `datetime.timedelta` | total seconds + marker | `timedelta` |
|
|
154
|
+
| `uuid.UUID` | string + marker | `UUID` |
|
|
155
|
+
| `decimal.Decimal` | string + marker | `Decimal` |
|
|
156
|
+
| `bytes` | base64 + marker | `bytes` |
|
|
157
|
+
| `bytearray` | base64 + marker | `bytearray` |
|
|
158
|
+
| `pathlib.Path` | string + marker | `Path` |
|
|
159
|
+
| `enum.Enum` | `.value` + marker | value only (enum class not preserved) |
|
|
160
|
+
| `tuple` | list + marker | `list` (documented: tuples become lists) |
|
|
161
|
+
| `set` / `frozenset` | sorted list + marker | `set` / `frozenset` |
|
|
162
|
+
| `dataclass` | `dataclasses.asdict()` then recurse | `dict` |
|
|
163
|
+
| `pydantic.BaseModel` | `.model_dump()` then recurse | `dict` |
|
|
164
|
+
| Circular reference | `{"__snapfix_circular__": true}` | sentinel dict |
|
|
165
|
+
| Unserializable type | `{"__snapfix_unserializable__": true, ...}` | sentinel dict |
|
|
166
|
+
|
|
167
|
+
> **Note:** `tuple` → `list` on roundtrip is intentional. JSON has no tuple type.
|
|
168
|
+
> If your tests depend on the exact type, assert `isinstance(result, list)` rather than `tuple`.
|
|
169
|
+
|
|
170
|
+
---
|
|
171
|
+
|
|
172
|
+
## Configuration
|
|
173
|
+
|
|
174
|
+
### Environment variables
|
|
175
|
+
|
|
176
|
+
| Variable | Default | Description |
|
|
177
|
+
|---|---|---|
|
|
178
|
+
| `SNAPFIX_OUTPUT_DIR` | `tests/fixtures` | Where fixture files are written |
|
|
179
|
+
| `SNAPFIX_MAX_DEPTH` | `10` | Maximum serialization depth |
|
|
180
|
+
| `SNAPFIX_MAX_SIZE` | `500000` | Maximum payload size in bytes |
|
|
181
|
+
| `SNAPFIX_ENABLED` | `true` | Set to `false` to disable all capture |
|
|
182
|
+
|
|
183
|
+
### `snapfix.yaml` (project root)
|
|
184
|
+
|
|
185
|
+
```yaml
|
|
186
|
+
snapfix:
|
|
187
|
+
output_dir: tests/fixtures
|
|
188
|
+
max_depth: 10
|
|
189
|
+
max_size_bytes: 500000
|
|
190
|
+
enabled: true
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
### Priority order (highest to lowest)
|
|
194
|
+
|
|
195
|
+
1. Decorator parameters: `@capture(name, scrub=[...], max_depth=5)`
|
|
196
|
+
2. Environment variables
|
|
197
|
+
3. `snapfix.yaml`
|
|
198
|
+
4. Built-in defaults
|
|
199
|
+
|
|
200
|
+
### Disabling in production
|
|
201
|
+
|
|
202
|
+
Set `SNAPFIX_ENABLED=false` in your production environment. The decorator becomes a no-op — zero overhead, no files written, no exceptions swallowed.
|
|
203
|
+
|
|
204
|
+
---
|
|
205
|
+
|
|
206
|
+
## CLI
|
|
207
|
+
|
|
208
|
+
```
|
|
209
|
+
snapfix list # List all captured fixtures with metadata
|
|
210
|
+
snapfix show <name> # Print a fixture to stdout
|
|
211
|
+
snapfix clear <name> # Delete one fixture (prompts for confirmation)
|
|
212
|
+
snapfix clear-all # Delete all fixtures (prompts for confirmation)
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
All commands accept `--dir <path>` to target a non-default fixture directory.
|
|
216
|
+
|
|
217
|
+
---
|
|
218
|
+
|
|
219
|
+
## Decorator reference
|
|
220
|
+
|
|
221
|
+
```python
|
|
222
|
+
@capture(
|
|
223
|
+
name, # str — fixture name; becomes the function name in the output file
|
|
224
|
+
scrub=None, # list[str] | None — extra field names to scrub (merged with defaults)
|
|
225
|
+
max_depth=None, # int | None — override max serialization depth
|
|
226
|
+
max_size_bytes=None, # int | None — override max payload size
|
|
227
|
+
config=None, # SnapfixConfig | None — full config override
|
|
228
|
+
)
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
Works on both **sync** and **async** functions. The decorator is transparent:
|
|
232
|
+
|
|
233
|
+
- The return value is always preserved unchanged.
|
|
234
|
+
- If the wrapped function raises, the exception propagates normally and **no file is written**.
|
|
235
|
+
- If serialization fails for any reason, a `warnings.warn()` is emitted and execution continues.
|
|
236
|
+
|
|
237
|
+
---
|
|
238
|
+
|
|
239
|
+
## FAQ
|
|
240
|
+
|
|
241
|
+
**Is it safe to use against production traffic?**
|
|
242
|
+
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.
|
|
243
|
+
|
|
244
|
+
**Does it slow down my application?**
|
|
245
|
+
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.
|
|
246
|
+
|
|
247
|
+
**What happens if the object is too large?**
|
|
248
|
+
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`.
|
|
249
|
+
|
|
250
|
+
**What happens if a field contains an unserializable type?**
|
|
251
|
+
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.
|
|
252
|
+
|
|
253
|
+
**Can I regenerate a fixture?**
|
|
254
|
+
Yes. Re-run the decorated function. The fixture file is overwritten in place.
|
|
255
|
+
|
|
256
|
+
**Does it work with pytest parametrize?**
|
|
257
|
+
The generated file is a standard `@pytest.fixture`. It works with everything pytest supports.
|
|
258
|
+
|
|
259
|
+
---
|
|
260
|
+
|
|
261
|
+
## Development
|
|
262
|
+
|
|
263
|
+
```bash
|
|
264
|
+
git clone https://github.com/yourname/snapfix
|
|
265
|
+
cd snapfix
|
|
266
|
+
pip install -e ".[dev]"
|
|
267
|
+
pytest
|
|
268
|
+
ruff check src/
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
---
|
|
272
|
+
|
|
273
|
+
## License
|
|
274
|
+
|
|
275
|
+
MIT
|