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.
@@ -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
@@ -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